How to Sign a Transaction
In CKB, there are two primary Lock Scripts
0x00
: Pay to Public Key Hash (P2PKH)0x01
: Pay to Multisignature (Multisig)
This how-to demonstrates how to sign P2PKH and multisig transactions.
Before you begin, ensure you have a solid grasp of:
- CKB transaction structure, as detailed in RFC0022: CKB Transaction Structure
- CKB Address Format, as detailed in RFC0021: CKB Address Format
P2PKH Signing
Required Arguments
- Private Key: a secp256k1 private key
- Witnesses: contain transaction signatures
Pseudo Code
def sign_tx(pk, tx):
# Group transaction inputs for signing
input_groups = compute_input_groups(tx.inputs)
# Iterate through each group of inputs
for indexes in input_groups:
group_index = indexes[0]
# Create a placeholder for the lock field in the first witness of the group
dummy_lock = [0] * 65 # Placeholder, assuming a 65-byte lock field
# Retrieve and deserialize the first witness in the group
witness = tx.witnesses[group_index]
witness_args = witness.deserialize()
# Replace the lock field with the dummy placeholder
witness_args.lock = dummy_lock
# Initialize a new BLAKE2b hasher for creating the signature hash
hasher = new_blake2b()
# Hash the transaction 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 remaining 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 additional witnesses 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)
# Finalize the hasher to get the signature hash
sig_hash = hasher.finalize()
# Sign the transaction with private key
signature = pk.sign(sig_hash)
# Replace the dummy lock field with the actual signature in the first witness
witness_args.lock = signature
# Serialize the updated witness_args and update the transaction's witnesses list
tx.witnesses[group_index] = serialize(witness_args)
Input group
This article focuses specifically on the input group associated with Lock Script. While there is a similar concept in Type Script
CKB processes transactions by grouping inputs based on the script_hash
of each input’s lock field, processing them collectively rather than individually.
For example, consider the following inputs:
inputs = [g1, g2, g1]
Here are three inputs, where the gN notation represents the group. inputs[0]
and inputs[2]
share the same group g1, while inputs[1]
is in a different group g2. CKB runs the Lock Script on g1 and g2 separately. Understanding that the same script_hash
implies identical lock fields of inputs, thus requiring only one signature per group.
The default locks, such as P2SH and multisig, assume there is a witnesswitnesses[0]
, while for g2, from witnesses[1]
.
The number of required witnesses corresponds to the number of input groups. For example:
- For inputs
[g1, g2, g1]
, two witnesses are needed:[witness1, witness2]
. - For inputs
[g1, g2, g1, g3]
, four witnesses are needed, including an empty byte to alignwitness3
for g3:[witness1, witness2, (empty bytes), witness3]
. - For inputs
[g1, g2, g2, g1]
, two witnesses are needed:[witness1, witness2]
.
A helper function grouping inputs by their script_hash
and returning the indexes could look like this:
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
The witness field of a transaction holds signatures, which are read by P2PKH Lock Scripts.
WitnessArgs
WitnessArgs
allows Lock Script and Type Script
The default P2PKH and the multisig Scripts require user to use WitnessArgs
on the first witness
of each group.
In CKB, witnesses
is extended to allow users to include any one-time data necessary solely for the transaction’s verification process. However, this flexibility brings a problem, a transaction's Lock Script and Type Script may require different data from the same witness, there is a risk that neither script’s requirement can be met at the same time, potentially causing the transaction permanently locked.
To dismiss this potential conflict, WitnessArgs
is introduced to allow Lock Script and Type Script to read data from different fields in the WitnessArgs
.
Multisig Signing
Much like P2PKH transactions, multisig signing is about providing signatures in witnesses to unlock the input. Plus, with each input group isolated, a single transaction can combine both multisig and P2PKH signing methods.
Required Arguments
- Private Keys: a set of secp256k1 private keys
- Witnesses: contain transaction signatures
- Multisig Script: a Script defining the multisig constraint
Pseudo Code
def sign_tx(pks, tx, multisig_script):
# Group the transaction inputs for signing
input_groups = compute_input_groups(tx.inputs)
# Iterate through each group of inputs
for indexes in input_groups:
group_index = indexes[0]
# Extract the required number of signatures 'm' from the multisig script
m = multisig_script[2]
# Create a dummy lock field with space for 'm' signatures
dummy_lock = multisig_script + [0] * 65 * m
# Deserialize the first witness and replace its lock field with the dummy lock
witness = tx.witnesses[group_index]
witness_args = witness.deserialize()
witness_args.lock = dummy_lock
# Initialize a new BLAKE2b hasher for the signature hash
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 remaining 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 additional witnesses 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)
# Finalize the hasher to get the signature hash
sig_hash = hasher.finalize()
# Sign the transaction with each private key
for (i, pk) in enumerate(pks):
signature = pk.sign(sig_hash)
# Place each signature in the appropriate position in the dummy lock field
sig_offset = len(multisig_script) + i * 65 # Calculate the starting index for the signature
witness_args.lock[sig_offset:sig_offset + 65] = signature
# Serialize the updated witness arguments and update the transaction's witnesses list
tx.witnesses[group_index] = serialize(witness_args)
Multisig Script
The multisig script hash is a 20-byte blake160 hash, assembled as follows:
S | R | M | N | blake160(Pubkey1) | blake160(Pubkey2) | ...
Here, S
/ R
/ M
/ N
are four single byte unsigned integers, ranging from 0 to 255.
S
is format version, set to 0.R
specifies that at least R of the provided signatures must match the first R items of the public key list.M
andN
indicate the required number of signatures, M of N, to unlock the cell.blake160 (Pubkey1)
represents the first 160 bits of the blake2b hash of secp256K1 compressed public keys.
For example, if Alice, Bob, and Cipher collectively control a multisig-locked cell with the unlocking rule "any two of us can unlock the cell, and one must be Cipher’s". The corresponding multisig script is:
0 | 1 | 2 | 3 | Pk_Cipher_h | Pk_Alice_h | Pk_Bob_h
To sign a transaction, the multisig script must be revealed in the witness field, followed by the corresponding signatures:
multisig_script | Signature1 | Signature2 | ...
Since
The since
field introduces a timelock constraint, dictating when the multisig lock can be unlocked. It is typically empty, indicating that there is no temporal restriction on unlocking the lock, allowing immediate access.
For an in-depth understanding and detailed exploration, refer to: RFC0017: Transaction Since Precondition and RFC0028: Change Since Relative Timestamp.