# Simple CKB Wallet Protocol ## Motivation As a layer 1 with Turing complete smart contract capability, there are several dApps running on CKB. However, due to CKB's crypto abstraction, the signature verifucation runs at the contract layer rather than the consensus layer, this gives the wallet great flexibility. There are already several wallets in the CKB ecosystem, such as the MetaMask-based [PW Wallet][pw], the DKIM-based [UniPass Wallet][unipass], the WebAuthn-based [JoyID][joyid], and the most commonly used BIP44-based wallets like [CKB-CLI][ckb-cli] and [Neuron][neuron]. Although these wallets have the ability to sign transactions, some even expose the capability externally, the lack of a unified pattern or standard results in almost every dApp having its own implementation for communication with wallets. The Web3 ecosystem already has several excellent and widely applicable proposals for wallet communication. This document will reference these proposals and establish patterns specific to CKB, aiming to simplify and unify communication between dApps and wallets. ## Workflow > `alt` - the corner mark to indicate "if(x) else if(y) ..." in the sequence diagram below ```mermaid sequenceDiagram Actor user as User participant wallet as Wallet participant dapp as DApp Note over user,dapp: Establishing Connection user ->> dapp: access activate dapp alt wallet/dapp on the same device dapp ->> wallet: connect_request<br />(methods,events) else wallet/dapp on the diff device dapp ->> user: QR code/deep link Note left of wallet: scan or paste user ->> wallet: QR code/deep link end alt first pairing wallet ->> user: permission_request<br />(methods,events) user ->> wallet: ok end wallet ->> dapp: user_identity<br />(address or something else) deactivate dapp Note over user,dapp: dApp Business Processing loop jsonrpc dapp ->> wallet: jsonrpc_request activate dapp alt approval needed wallet ->> user: method,params user ->> wallet: ok end wallet ->> dapp: jsonrpc_response deactivate dapp end ``` ## Establishing Connection via WalletConnect [WalletConnect][walletconnect] is a protocol for connecting wallets to dApps. It is well-document and widely adopted in the Web3 ecosystem. It usually used when the wallet and the dApp are on different devices. ### Session Namespaces To establish a connection via WalletConnect, the [`Session Namespaces`](https://docs.walletconnect.com/2.0/specs/clients/sign/namespaces) SHOULD be defined as follows: ```json5 { requiredNamespaces: { ckb: { // the `chains` is required by WalletConnect chains: ["ckb:mainnet"], // or "ckb:testnet" methods: ["ckb_getAddresses", "ckb_signTransaction", "..."], events: [], }, }, } ``` And the received `Session Namespaces`: ```json5 { sessionNamespaces: { ckb: { accounts: ["ckb:mainnet:user_identity"], methods: ["ckb_getAddresses", "ckb_signTransaction", "..."], events: [], }, }, } ``` The `user_identity` in the `accounts` MAY be an address or something that can be used to identify the user. We cannot just use the address as the `user_identity` because of addresses in a BIP44 wallet are derived from a seed, and addresses are always appending, so the `user_identity` depends on the wallet implementation. ## Establishing Connection via Injected Provider > TODO, this is not in a high priority ## Data Structure and Methods ### `Address` `Address` is a string in [full payload format address][full-payload-format-address] ```typescript type Address = string; ``` An address includes information about - the network type - what lock script is used - how to verify the signature - public key or hash of public key ### `ckb_getAddresses` CKB uses UTxO model, so a CKB wallet MAY have multiple addresses, even for a transaction, the inputs could be from different addresses. To get the addresses which are actually needed for building a transaction, the dApp may need to filter the addresses ```typescript type ckb_getAddresses = (options?: object) => Promise<Address[]>; ``` The `ckb_getAddresses` method is designed to accept an `options` parameter, which is an object. The shape of the `options` object is dependent on the wallet implementation, and the wallet SHOULD document the options it supports. ### `TransactionTemplate` An `Address` in `inputs` that needs to be unlocked by a signature, the signature is usually placed in the `witnesses` field of the transaction. To execute the script in the transaction, the `cellDeps` are also needed. ```typescript type TransactionTemplate = { cellDeps: CellDep[]; witnessLock: HexString; }; ``` - `cellDeps` is an array of `CellDep`, which MAY be the code cell of the lock script, or the data cell for providing data for the lock script, e.g. the secp256k1 precomputed table, or a merkle root storage cell. - `witnessLock` is the witness for unlocking the lock script, it MAY be a signature placeholder, or a merkle proof. ### `ckb_getTransactionTemplate` The `witnesses` in a transaction is not just for signatures, it may also contain other data, such as the merkle proof when unlocking special lock scripts, such as the [Taproot][taproot-script-witness] and the [UniPass](https://github.com/UniPassID/up-ckb/blob/23b42cbcd63a29ecd4ca6f2d871c2c77baeeb978/src/js-scripts/up-lock-witness.js#LL157C26-L157C26). ```typescript type ckb_getTransactionTemplate = ( address: Address, options?: object ) => Promise<TransactionTemplate>; ``` DApp COULD call the `ckb_getTransactionTemplate` method when it needs to build a transaction, once the `TransactionTemplate` is received, the dApp COULD append the `TransactionTemplate` to the transaction before signing it. ### `ckb_signTransaction` ```typescript type ckb_signTransaction = ( transaction: Transaction, options?: object ) => Promise<Transaction>; ``` The `ckb_signTransaction` method is designed to sign a transaction, the `options` parameter is optional, and the shape of the `options` object is dependent on the wallet implementation, and the wallet SHOULD document the options it supports. ### `ckb_combineSignTransaction` The `*sig_all` lock is good for security because of the simple rule which is just verifying the signature, therefore, there are many scripts based on the `*sig_all` lock, such as the [Omnilock][omnilock], [Cheque Lock][cheque-lock], and a new lock called Combine Lock which is just in the design phase. These scripts are act as a proxy to the `*sig_all` lock and append some extra rules for unlocking the lock script. To make the wallet support signing transactions with these kinds of scripts, dApp could let the wallet know the signing positions, and the wallet SHOULD treat the `SigningPosition` as the `*sig_all` lock, and follow the signing process to sign the transaction. ```typescript type SigningPosition = [ Address, number // index of the inputs in the transaction ]; ``` Instead of returning a signed transaction, the `ckb_combineSignTransaction` method returns the signature for each signing position. And the dApp SHOULD replace the signature placeholder the actual signature. ```typescript type Signature = HexString; ``` There MAY be more than one signature are needed in a transaction, so the `ckb_combineSignTransaction` method returns an array of `[Address, Signature]` tuples for each signing position. ```typescript type ckb_combineSignTransaction = ( transaction: Transaction, signingPositions: SigningPosition[] ) => Promise<[Address, Signature][]>; ``` ### `personal_sign` Sign a message with a magic prefix [`"Nervos Message:"`](https://github.com/nervosnetwork/neuron/blob/59509826be74f7942c5335f521fa632223c719b6/packages/neuron-wallet/src/services/sign-message.ts#L14). It is helpful for sign in a non-decentralized service. ```typescript type personal_sign = ( address: Address, message: HexString | UTF8String ) => Promise<HexString>; ``` The wallet SHOULD display the message to the user, and the user SHOULD approve the message before signing it. ## Example For Adapting Neuron Wallet ### Simple Transfer(Pseudocode) ```typescript type Secp256k1SigHashAllOptions = { type: "secp256k1_blake160_sighash_all"; // defaults to false // indicates whether the address has a transaction history hasTransactionHistory?: boolean; // indicates whether the address is change(internal) address // @see BIP44 change?: "external" | "internal"; }; type Secp256k1MultisigAllOptions = { type: "secp256k1_blake160_multisig_all"; s?: number; r?: number; m?: number; n?: number; publicKeyHashes?: Hash[]; }; type impl_ckb_getAddresses = ( options?: Secp256k1SigHashAllOptions | Secp256k1MultisigAllOptions ) => Promise<Address[]>; ``` ```typescript // pairing with Neuron Wallet walletconnect.pair({ projectId: "...", requiredNamespaces: { ckb: { chains: ["ckb:mainnet"], methods: [ "ckb_getAddresses", "ckb_getTransactionTemplate", "ckb_signTransaction", ], }, }, }); declare const provider: NeuronProvider; const usedAddresses = await provider.getAddresses({ hasTransactionHistory: true, }); const changeAddress = ( await provider.getAddresses({ hasTransactionHistory: false, }) )[0]; const txTemplate = provider.getTransactionTemplate(usedAddresses[0]); const liveCells = await provider.collect(_10000ckb, usedAddresses); await provider.signTransactoin({ cellDeps: txTemplate.cellDeps, inputs: liveCells, outputs, // each different input address should have a signature placeholder in witness.lock witnesses, ... }); ``` ### With Combine Lock(Pseudocode) ```typescript // will hook the provider // - getAddresses - get original address from wallet and transform to combine lock address // - getTransactionTemplate - transform the original transaction template to combine lock transaction template // - signTransaction - transform the original transaction to combine lock transaction, and call the `ckb_combineSignTransaction` method type AdaptCombineLock = (provider: Provider) => Provider; ``` ## Misc <details> <summary>random idea, library, references...</summary> - [EIP155 Simple replay attack protection](https://eips.ethereum.org/EIPS/eip-155) ``` CHAIN_ID: 1(mainnet) v = CHAIN_ID * 2 + 35 or v = CHAIN_ID * 2 + 36 ``` - [BIP122 URI scheme for Blockchain references](https://github.com/bitcoin/bips/blob/master/bip-0122.mediawiki) ```yaml blockchain:/block/00000000000000000119af5bcae2926df54ae262e9071a94a99c913cc217cc72 blockchain:/address/16EW6Rv9P9AxFDBrZV816dD4sj1EAYUX3f ``` - [Blockchain ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) ``` chain_id: namespace + ":" + reference namespace: [-a-z0-9]{3,8} reference: [-_a-zA-Z0-9]{1,32} ``` ```yaml # indicate Bitcoin mainnet bip122:000000000019d6689c085ae165831e93 ``` - Variant BIP44 [CKB-CLI](https://github.com/nervosnetwork/ckb-cli/blob/856875f2bb46afb5db75c7b0fa431e0f10a73f8c/ckb-signer/src/keystore/mod.rs#L35), [Neuron](https://github.com/nervosnetwork/neuron/blob/df6bf9127624474d9146830b7b84eee4304297ee/packages/neuron-wallet/src/models/keys/key.ts#L53) ``` m/44'/309'/0'/change/address_index ``` - [How to sign transaction](https://github.com/nervosnetwork/ckb-system-scripts/wiki/How-to-sign-transaction) ``` digest = hash( hash(tx) || len(witness) || witnesses[1:] ) signature = sign(digest) ``` ### Any Solution For More Flexible? Only communicate with public key, and define a set of [codecs](https://github.com/multiformats/multicodec/blob/master/table.csv) for the wallet to create a digest from the transaction, and sign it. ``` # caculate tx hash <tx.json> <blockchain.mol> CODEC_MOL "RawTransaction" CODEC_BLAKE2B_256 ``` And request signing via JSON-RPC ```json5 { method: "ckb_sign", params: [ // a public key to indicate which key to use for signing "0xpubKey", // a set of codecs '<tx.json>\n<blockchain.mol>\nCODEC_MOL "RawTransaction"\nCODEC_BLAKE2B_256', // transaction "...", // blockchain.mol "...", ], } ``` The codecs MUST NOT modify the input data, and only used for transform the data, so the wallet can display the raw data to the user for confirmation to ensure that the user is not phished. ### Options is Ambiguous? - it is not always necessary for all lock scripts - add some APIs to describe the [options](https://docs.swagger.io/spec.html#5231-object-example), `ckb_getAddressesOptionsDescription`... </details> [joyid]: https://docs.joy.id [ckb-cli]: https://github.com/nervosnetwork/ckb-cli [neuron]: https://github.com/nervosnetwork/neuron [pw]: https://github.com/lay2dev/ckb.pw [unipass]: https://github.com/UniPassID/up-ckb [walletconnect]: https://docs.walletconnect.com/2.0 [full-payload-format-address]: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0021-ckb-address-format/0021-ckb-address-format.md#full-payload-format [taproot-script-witness]: https://blog.cryptape.com/enable-bitcoin-taproot-on-ckb-part-ii#heading-taproot-witness [omnilock]: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0042-omnilock/0042-omnilock.md#authentication [cheque-lock]: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0039-cheque/0039-cheque.md