Signing transactions
Before a transaction can be sent to the Bitcoin network, each input must be signed.
Threshold ECDSA
Canisters can sign transactions with threshold ECDSA through the
sign_with_ecdsa
method.
To test canisters locally that use the following code snippets, you will need to enable local Bitcoin development. To do this, you can either start the local development environment with dfx start --enable-bitcoin
or you can include the following configuration in the project's dfx.json
file:
"defaults": {
"bitcoin": {
"enabled": true,
"nodes": [
"127.0.0.1:18444"
],
"log_level": "info"
},
The following snippet shows a simplified example of how to sign a Bitcoin transaction for the special case where all the inputs are referencing outpoints that are owned by own_address
and own_address
is a P2PKH
address.
- Motoko
- Rust
public func sign_transaction(
own_public_key : [Nat8],
own_address : BitcoinAddress,
transaction : Transaction,
key_name : Text,
derivation_path : [Blob],
signer : SignFun,
) : async [Nat8] {
// Obtain the scriptPubKey of the source address which is also the
// scriptPubKey of the Tx output being spent.
switch (Address.scriptPubKey(#p2pkh own_address)) {
case (#ok scriptPubKey) {
let scriptSigs = Array.init<Script>(transaction.txInputs.size(), []);
// Obtain scriptSigs for each Tx input.
for (i in Iter.range(0, transaction.txInputs.size() - 1)) {
let sighash = transaction.createSignatureHash(
scriptPubKey, Nat32.fromIntWrap(i), SIGHASH_ALL);
let signature_sec = await signer(key_name, derivation_path, Blob.fromArray(sighash));
let signature_der = Blob.toArray(Der.encodeSignature(signature_sec));
// Append the sighash type.
let encodedSignatureWithSighashType = Array.tabulate<Nat8>(
signature_der.size() + 1, func (n) {
if (n < signature_der.size()) {
signature_der[n]
} else {
Nat8.fromNat(Nat32.toNat(SIGHASH_ALL))
};
});
// Create Script Sig which looks like:
// ScriptSig = <Signature> <Public Key>.
let script = [
#data encodedSignatureWithSighashType,
#data own_public_key
];
scriptSigs[i] := script;
};
// Assign ScriptSigs to their associated TxInputs.
for (i in Iter.range(0, scriptSigs.size() - 1)) {
transaction.txInputs[i].script := scriptSigs[i];
};
};
// Verify that our own address is P2PKH.
case (#err msg)
Debug.trap("This example supports signing p2pkh addresses only.");
};
transaction.toBytes()
};
// Sign a P2PKH bitcoin transaction.
//
// IMPORTANT: This method is for demonstration purposes only and it only
// supports signing transactions if:
//
// 1. All the inputs are referencing outpoints that are owned by `own_address`.
// 2. `own_address` is a P2PKH address.
pub async fn sign_transaction<SignFun, Fut>(
ctx: &BitcoinContext,
own_public_key: &PublicKey,
own_address: &Address,
mut transaction: Transaction,
derivation_path: Vec<Vec<u8>>,
signer: SignFun,
) -> Transaction
where
SignFun: Fn(String, Vec<Vec<u8>>, Vec<u8>) -> Fut,
Fut: std::future::Future<Output = SecpSignature>,
{
assert_eq!(
own_address.address_type(),
Some(AddressType::P2pkh),
"Only P2PKH addresses are supported"
);
let transaction_clone = transaction.clone();
let sighash_cache = SighashCache::new(&transaction_clone);
for (index, input) in transaction.input.iter_mut().enumerate() {
let sighash = sighash_cache
.legacy_signature_hash(
index,
&own_address.script_pubkey(),
EcdsaSighashType::All.to_u32(),
)
.unwrap();
let signature = signer(
ctx.key_name.to_string(),
derivation_path.clone(),
sighash.as_byte_array().to_vec(),
)
.await;
let mut signature = signature.serialize_der().to_vec();
signature.push(EcdsaSighashType::All.to_u32() as u8);
let sig_bytes = PushBytesBuf::try_from(signature).unwrap();
let pubkey_bytes = PushBytesBuf::try_from(own_public_key.to_bytes()).unwrap();
input.script_sig = Builder::new()
.push_slice(sig_bytes)
.push_slice(pubkey_bytes)
.into_script();
input.witness = Witness::new();
}
transaction
}
Threshold Schnorr
Canisters can sign transactions with threshold Schnorr through the
sign_with_schnorr
method.
Signing P2TR key path transactions
The following snippet shows a simplified example of how to sign a Bitcoin
transaction for the special case where all the inputs are referencing outpoints
that are owned by own_address
and own_address
is a P2TR
address.
- Rust
public func sign_key_spend_transaction(
schnorr_canister_actor : SchnorrCanisterActor,
own_address : BitcoinAddress,
transaction : Transaction,
amounts : [Nat64],
key_name : Text,
derivation_path : [Blob],
aux : ?Types.SchnorrAux,
signer : Types.SchnorrSignFunction,
) : async [Nat8] {
// Obtain the scriptPubKey of the source address which is also the
// scriptPubKey of the Tx output being spent.
switch (Address.scriptPubKey(#p2tr_key own_address)) {
case (#ok scriptPubKey) {
assert scriptPubKey.size() == 2;
// Obtain a witness for each Tx input.
for (i in Iter.range(0, transaction.txInputs.size() - 1)) {
let sighash = transaction.createTaprootKeySpendSignatureHash(
amounts,
scriptPubKey,
Nat32.fromIntWrap(i),
);
let signature = Blob.toArray(await signer(schnorr_canister_actor, key_name, derivation_path, Blob.fromArray(sighash), aux));
transaction.witnesses[i] := [signature];
};
};
// Verify that our own address is P2TR key spend address.
case (#err msg) Debug.trap("This example supports signing p2tr key spend addresses only: " # msg);
};
transaction.toBytes();
};
Signing P2TR script path transactions
The following snippet shows a simplified example of how to sign a Bitcoin
transaction for the special case where all the inputs are referencing outpoints
that are owned by own_address
and own_address
is a P2TR
script path
address.
- Rust
// Sign a P2TR script spend transaction.
//
// IMPORTANT: This method is for demonstration purposes only and it only
// supports signing transactions if:
//
// 1. All the inputs are referencing outpoints that are owned by `own_address`.
// 2. `own_address` is a P2TR address that includes a script.
pub async fn sign_transaction_script_spend<SignFun, Fut>(
ctx: &BitcoinContext,
own_address: &Address,
mut transaction: Transaction,
prevouts: &[TxOut],
control_block: &ControlBlock,
script: &ScriptBuf,
derivation_path: Vec<Vec<u8>>,
signer: SignFun,
) -> Transaction
where
SignFun: Fn(String, Vec<Vec<u8>>, Option<Vec<u8>>, Vec<u8>) -> Fut,
Fut: std::future::Future<Output = Vec<u8>>,
{
assert_eq!(own_address.address_type(), Some(AddressType::P2tr),);
for input in transaction.input.iter_mut() {
input.script_sig = ScriptBuf::default();
input.witness = Witness::default();
input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME;
}
let num_inputs = transaction.input.len();
for i in 0..num_inputs {
let mut sighasher = SighashCache::new(&mut transaction);
let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript);
let signing_data = sighasher
.taproot_script_spend_signature_hash(
i,
&bitcoin::sighash::Prevouts::All(prevouts),
leaf_hash,
TapSighashType::Default,
)
.expect("Failed to encode signing data")
.as_byte_array()
.to_vec();
let raw_signature = signer(
ctx.key_name.to_string(),
derivation_path.clone(),
None,
signing_data.clone(),
)
.await;
// Update the witness stack.
let witness = sighasher.witness_mut(i).unwrap();
witness.clear();
let signature = bitcoin::taproot::Signature {
signature: Signature::from_slice(&raw_signature).expect("failed to parse signature"),
sighash_type: TapSighashType::Default,
};
witness.push(signature.to_vec());
witness.push(script.to_bytes());
witness.push(control_block.serialize());
}
transaction
}