RFC Draft: CKB Offline Transaction Signing Scheme

CKB Offline Transaction Signing Scheme

This document proposes a spec for securely signing CKB transactions offline. It could be used in a place where the highest level security is required. Transaction assembling and transaction signing should be separated and performed on 2 different machines.

In this scheme, there are 3 participating parties:

  1. A transaction assembler prepares an UnsignedTransaction based on user actions.
  2. The transaction assembler then sends UnsignedTransaction to signer, which is actually a program, a hardware wallet, or an air-gapped computer managing the private key.
  3. The signer visualizes the UnsignedTransaction to the user, who is a human owning the private key, the user inspects the transaction, and ensures the transaction fulfills his/her original intention.
  4. After the user confirms the transaction, the signer generates the message to be signed, signs it using the user’s specified private key, the final transaction can then be assembled for the next step.

Data Structure

An unsigned transaction is described via the following molecule formatted UnsignedTransaction data structure.

vector HeaderVec <Header>;
vector TransactionVec <Transaction>;

struct SighashAllSigning {
    signing_script: Script,
}

table LuaSigning {
    source: Bytes,
    args: Bytes,
}

union SigningMethods {
    SighashAllSigning,
    LuaSigning,
}

table UnsignedTransaction {
    signing_method: SigningMethods,
    tx: Transaction,
    input_txs: TransactionVec,
    cell_dep_txs: TransactionVec,
    headers: HeaderVec,
}

See here for definitions for CKB data structures, such as Transaction, Header or Script.

Upon receiving the UnsignedTransaction data structure, a signer shall perform the following operations:

  • Validate the full UnsignedTransaction structure.
  • Visualize the transaction to be signed for user to double confirm. For example, a signer might visualize a transferring operation, so user is full aware of the sender, receiver, amount to be transferred, as well as whereabouts of changes. For a Nervos DAO operation, more information shall be presented to the user.
  • Once the user confirms the transaction, the signer calculates the signing message, based on signing_method specification as well as the provided and confirmed transaction. Right now 2 signing methods are supported:
    • SighashAll: this is the default signing solution used in CKB now. See here for an explanation.
    • LuaSigning: a transaction assembler might provide a Lua script, the Lua script is then executed given current transaction(but transformed to a Lua table data structure), and the given args. The Lua script is expected to return a 0x-prefixed 66-byte long hex string, containing the actual message to be signed.
  • Now the signer gets the signing message, it might prompt the user for a double confirmation, once confirmed, the signer will signs the message with the private key, the signature can then be provided to transaction assembler for further actions.

Design Considerations

Why including full transactions for inputs and cell deps?

CKB does not contain information for input cells. For example, CKB capacities, or sUDT amounts are unknown given a single transaction. We will need the resolved input cells, so as to check that the transferred capacities or sUDT amounts are correct. However, given a single input cell is not enough to validate the input cell is correct, we will need the full transaction, so the offline signer can validate the correctness of input cells, by running a transaction hash check.

With the whole transaction for each cell included, the offline signer can be sure that the transaction indeed fulfills the specific purpose. All the involving parties can only choose to either include the signed transaction, or discarded, no forms of malleability can be performed.

Why molecule?

It’s perfectly possible to use plain JSON for serialization of UnsignedTransaction, however molecule, as a widely used data structure in CKB, has its unique property that memory consumption could be minimized, this will be particularly nice for embedded devices such as hardware wallets, or Raspberry Pi Pico style machines. Which means the offline signing scheme will have far more use cases.

Why Lua?

We could of course debate the pros and cons of each individual programming language. But in practice, Lua is the only programming language providing an industry grade secure sandbox feature. At the same time, it also has the benefit that minimal resource is required to execute Lua code, which is far less than other sandbox solutions such as Docker, WASM or even CKB-VM.

So while we can enrich this list with more signing methods and other languages that are proven to be secure, Lua, as of now, provides a good and working solution for us.

Note

As a side project, I do plan to build a Raspberry Pi Pico powered, air gapped offline transaction signing app. With a Cortex-M0+ chip and 264 KB RAM, the Pico should be more than enough to power such an offline signing design.

1 Like