Skip to content

How to sign transaction

jjy edited this page Nov 21, 2020 · 6 revisions

Signing

Currently, CKB has two lock scripts in the genesis block, The address code hash index 0x00 is for pay to pubkey hash and 0x01 is for pay to multisig, see RFC to learn details.

This document demonstrates how to sign P2PKH transactions.

Before continuing, make sure you already understand the transaction structure. (The details in this RFC maybe outdated, but still worth to read, you can check the newest transaction structure from the schema)

P2PKH

We need the following arguments to sign a tx:

* pk, secp256k1 private key
* witnesses, contains signatures of the tx.

Before sign a tx, we need to know some basic concepts:

Input group - For a tx, instead verify each input, CKB group the inputs by compute the script_hash of the lock field of input, then run the unlock script by the group.

For example, here we have some inputs:

inputs = [g1, g2, g1]

We have 3 inputs, the gN notation represents the group, the inputs[0] and inputs[2] are in the same group g1, the inputs[1] is in g2 which is another group.

CKB run lock script on g1 and g2 separately. You may notice, the same script_hash requirement also implies the lock field of inputs is also the same, so we only need to provide one signature for each group.

The default locks P2SH and multisig assume for each input group there exists a witness to unlock the input(which means the witness contains the signature), the default locks will try to read signature from the index of the first input. For the g1 the lock script will try to read signature from witnesses[0], because the index of the first input of g1 is 0, and g2 will try to read the witnesses[1].

For example:

  • For inputs [g1, g2, g1], we need 2 witnesses [witness1, witness2].
  • For inputs [g1, g2, g1, g3], we need 4 witnesses includes an empty bytes to align witness3 for g3 [witness1, witness2, (empty bytes), witness3].
  • For inputs [g1, g2, g2, g1], we need witnesses [witness1, witness2].

This helper function group inputs and return the indexes by the input group.

def compute_input_groups(inputs):
    groups = defaultdict(list)
    for i, input in enumerate(inputs):
        script_hash = blake256(serialize(input.lock))
        groups[script_hash].append(i)

    return groups.values()

Witness - Tx's witnesses field is originally designed to put signatures. P2PKH lock script will try to read signatures from witnesses.

WitnessArgs - In CKB the concept of witnesses is extended to allow a user to put any one-time data, these data are only required for the current tx verification.

The flexible of CKB contract also brings a problem, tx's lock and type contract maybe require different data on the same witness, neither scripts can get satisfied at the same time, the tx may be locked forever.

To dismiss the potential conflict, CKB provides a structure WitnessArgs, lock script and type script read data from different fields of WitnessArgs, the default P2PKH and the multisig scripts require user use WitnessArgs on the first witness of each group.

# Pseudo code to sign a tx of P2PKH
def sign_tx(pk, tx):
    input_groups = compute_input_groups(tx.inputs)
    # signing by groups.
    for indexes in input_groups:
        group_index = indexes[0]
        # make a reserve for the lock field of first witness in group
        dummy_lock = [0] * 65
        witness = tx.witnesses[group_index]
        witness_args = witness.deserialize()
        witness_args.lock = dummy_lock
        hasher = new_blake2b()
        # hash the tx hash
        hasher.update(tx.hash())
        # hash the first witness
        witness_len_bytes = len(serialize(witness_args)).to_le()
        assert(len(witness_len_bytes), 8)
        # hash the length of witness
        hasher.update(witness_len_bytes)
        hasher.update(serialize(witness_args))
        # hash the other witnesses in the group
        for i in indexes[1:]:
            witness = tx.witnesses[i]
            witness_len_bytes = len(witness).to_le()
            assert(len(witness_len_bytes), 8)
            hasher.update(witness_len_bytes)
            hasher.update(witness)
        # hash witnesses which do not in any input group
        for witness in tx.witnesses[len(tx.inputs):]
            witness_len_bytes = len(witness).to_le()
            assert(len(witness_len_bytes), 8)
            hasher.update(witness_len_bytes)
            hasher.update(witness)
        sig_hash = hasher.finalize()
        # sign tx
        signature = pk.sign(sig_hash)
        # put the signature back to the first witness
        witness_args.lock = signature
        tx.witnesses[group_index] = serialize(witness_args)

Multisig

We need the following arguments to sign a tx:

* pks, secp256k1 private keys
* witnesses, contains signatures of the tx.
* multisig script, a simple script describes the multisig constraint.

We already know how to sign the P2PKH tx, multisig is quite the same, we just need to provide signatures in witnesses to unlock the input. Actually, since each input group is isolated, we can use both multisig and P2PKH in a tx.

Before sign a multisig tx, we need to know some basic concepts:

Multisig script - The multisig script hash is the 20 bytes blake160 hash of multisig script. The multisig script should be assembled in the following format:

S | R | M | N | blake160(Pubkey1) | blake160(Pubkey2) | ...

Where S/R/M/N are four single byte unsigned integers, ranging from 0 to 255, and blake160(Pubkey1) it the first 160bit blake2b hash of SECP256K1 compressed public keys. S is format version, currently fixed to 0. M/N means the user must provide M of N signatures to unlock the cell. And R means the provided signatures at least match the first R items of the Pubkey list.

For example, Alice, Bob, and Cipher collectively control a multisig locked cell. They define the unlock rule like "any two of us can unlock the cell, but Cipher must approve". The corresponding multisig script is:

0 | 1 | 2 | 3 | Pk_Cipher_h | Pk_Alice_h | Pk_Bob_h

When sign a tx, we need to reveal the multisig script by putting it in witness:

multisig_script | Signature1 | Signature2 | ...

Since - A timelock constraint, normally it should be empty which means we can unlock multisig lock anytime. see since RFC for details.

NOTICE!!! Your coin may be locked forever due to lock script and type script requirements conflict!!! the since constraint support three timelock types: timestamp / epoch / block number, but the current DAO contract requires the timelock type of since to be epoch. This means a lock script require timestamp or block number timelock type can not be used with DAO contract, otherwise your coin will be locked forever.

You should beware when you use a type script on cell output.

# Pseudo code to sign a tx of multisig
def sign_tx(pks, tx, multisig_script):
    input_groups = compute_input_groups(tx.inputs)
    # signing by groups.
    for indexes in input_groups:
        group_index = indexes[0]
        # extract m from multisig script
        m = multisig_script[2]
        # reserve for m signatures
        dummy_lock = multisig_script + [0] * 65 * m
        witness = tx.witnesses[group_index]
        witness_args = witness.deserialize()
        witness_args.lock = dummy_lock
        hasher = new_blake2b()
        # hash the tx hash
        hasher.update(tx.hash())
        # hash the first witness
        witness_len_bytes = len(serialize(witness_args)).to_le()
        assert(len(witness_len_bytes), 8)
        # hash the length of witness
        hasher.update(witness_len_bytes)
        hasher.update(serialize(witness_args))
        # hash the other witnesses in the group
        for i in indexes[1:]:
            witness = tx.witnesses[i]
            witness_len_bytes = len(witness).to_le()
            assert(len(witness_len_bytes), 8)
            hasher.update(witness_len_bytes)
            hasher.update(witness)
        # hash witnesses which do not in any input group
        for witness in tx.witnesses[len(tx.inputs):]
            witness_len_bytes = len(witness).to_le()
            assert(len(witness_len_bytes), 8)
            hasher.update(witness_len_bytes)
            hasher.update(witness)
        sig_hash = hasher.finalize()
        # sign tx
        for (i, pk) in enumerate(pks):
            signature = pk.sign(sig_hash)
            # put the signature back to the first witness
            sig_offset = len(multisig_script) + i * 65
            witness_args.lock[sig_offset:sig_offset + 65] = signature
        tx.witnesses[group_index] = serialize(witness_args)
Clone this wiki locally