Constructing and Signing Transactions - Taproot
In this section, we will construct a Taproot transaction.
This is the cargo
commands that you need to run this example:
cargo add bitcoin --features "std, rand-std"
First we'll need to import the following:
use bitcoin::hashes::Hash;
use bitcoin::key::{Keypair, TapTweak, TweakedKeypair, UntweakedPublicKey};
use bitcoin::locktime::absolute;
use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing, Verification};
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
use bitcoin::{
transaction, Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut,
Txid, Witness,
};
Here is the logic behind these imports:
bitcoin::key
is used to tweak keys according to BIP340bitcoin::hashes::Hash
is used to hash databitcoin::locktime::absolute
is used to create a locktimebitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing, Verification}
is used to sign transactionsuse bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}
is used to create and tweak taproot sighashesbitcoin::{Address, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness}
is used to construct transactions
Next, we define the following constants:
use bitcoin::Amount;
const DUMMY_UTXO_AMOUNT: Amount = Amount::from_sat(20_000_000);
const SPEND_AMOUNT: Amount = Amount::from_sat(5_000_000);
const CHANGE_AMOUNT: Amount = Amount::from_sat(14_999_000); // 1000 sat fee.
DUMMY_UTXO_AMOUNT
is the amount of the dummy UTXO we will be spendingSPEND_AMOUNT
is the amount we will be spending from the dummy UTXOCHANGE_AMOUNT
1 is the amount we will be sending back to ourselves as change
Before we can construct the transaction, we need to define some helper functions2:
use bitcoin::secp256k1::{rand, Secp256k1, SecretKey, Signing};
use bitcoin::key::Keypair;
fn senders_keys<C: Signing>(secp: &Secp256k1<C>) -> Keypair {
let sk = SecretKey::new(&mut rand::thread_rng());
Keypair::from_secret_key(secp, &sk)
}
senders_keys
generates a random private key and derives the corresponding public key hash.
This will be useful to mock a sender.
In a real application these would be actual secrets3.
We use the SecretKey::new
method to generate a random private key sk
.
We then use the Keypair::from_secret_key
method to instantiate a Keypair
type,
which is a data structure that holds a keypair consisting of a secret and a public key.
Note that senders_keys
is generic over the Signing
trait.
This is used to indicate that is an instance of Secp256k1
and can be used for signing.
use bitcoin::{Address, Network};
fn receivers_address() -> Address {
"bc1p0dq0tzg2r780hldthn5mrznmpxsxc0jux5f20fwj0z3wqxxk6fpqm7q0va".parse::<Address<_>>()
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
}
receivers_address
generates a receiver address.
In a real application this would be the address of the receiver.
We use the parse
method on &str
to parse "bc1p0dq0tzg2r780hldthn5mrznmpxsxc0jux5f20fwj0z3wqxxk6fpqm7q0va"
4 as an address.
Note that bc1p0dq0tzg2r780hldthn5mrznmpxsxc0jux5f20fwj0z3wqxxk6fpqm7q0va
is a Bech32 address.
This is an arbitrary, however valid, Bitcoin mainnet address.
Bitcoin applications are usually configured with specific Bitcoin network at the start and use that.
To prevent mistakes related to people sending satoshis to a wrong network we need to call the require_network
method to ensure that the address is valid for the network, in our case mainnet.
use bitcoin::{Amount, OutPoint, ScriptBuf, TxOut, Txid};
use bitcoin::hashes::Hash;
use bitcoin::key::UntweakedPublicKey;
use bitcoin::locktime::absolute;
use bitcoin::secp256k1::{Secp256k1, Verification};
const DUMMY_UTXO_AMOUNT: Amount = Amount::from_sat(20_000_000);
fn dummy_unspent_transaction_output<C: Verification>(
secp: &Secp256k1<C>,
internal_key: UntweakedPublicKey,
) -> (OutPoint, TxOut) {
let script_pubkey = ScriptBuf::new_p2tr(secp, internal_key, None);
let out_point = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 0,
};
let utxo = TxOut { value: DUMMY_UTXO_AMOUNT, script_pubkey };
(out_point, utxo)
}
dummy_unspent_transaction_output
generates a dummy unspent transaction output (UTXO).
This is a P2TR (ScriptBuf::new_p2tr
) UTXO.
It takes the following arguments:
secp
is a reference to aSecp256k1
type. This is used to verify the internal key.internal_key
is aUntweakedPublicKey
type. This is the internal key that is used to generate the script pubkey. It is untweaked, since we are not going to tweak the key.merkle_root
is an optionalTapNodeHash
type. This is the merkle root of the taproot tree. Since we are not using a merkle tree, we are passingNone
.
Verification
is a trait that indicates that an instance of Secp256k1
can be used for verification.
The UTXO has a dummy invalid transaction ID (txid: Txid::all_zeros()
),
and a value of the const DUMMY_UTXO_AMOUNT
that we defined earlier.
P2TR UTXOs could be tweaked (TweakedPublicKey
)
or untweaked (UntweakedPublicKey
).
We are using the latter, since we are not going to tweak the key.
We are using the OutPoint
struct to represent the previous transaction output.
Finally, we return the tuple (out_point, utxo)
.
Now we are ready for our main function that will sign a transaction that spends a p2tr
unspent output:
use bitcoin::hashes::Hash;
use bitcoin::key::{Keypair, TapTweak, TweakedKeypair, UntweakedPublicKey};
use bitcoin::locktime::absolute;
use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing, Verification};
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
use bitcoin::{
transaction, Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut,
Txid, Witness,
};
const DUMMY_UTXO_AMOUNT: Amount = Amount::from_sat(20_000_000);
const SPEND_AMOUNT: Amount = Amount::from_sat(5_000_000);
const CHANGE_AMOUNT: Amount = Amount::from_sat(14_999_000); // 1000 sat fee.
fn senders_keys<C: Signing>(secp: &Secp256k1<C>) -> Keypair {
let sk = SecretKey::new(&mut rand::thread_rng());
Keypair::from_secret_key(secp, &sk)
}
fn receivers_address() -> Address {
"bc1p0dq0tzg2r780hldthn5mrznmpxsxc0jux5f20fwj0z3wqxxk6fpqm7q0va".parse::<Address<_>>()
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
}
fn dummy_unspent_transaction_output<C: Verification>(
secp: &Secp256k1<C>,
internal_key: UntweakedPublicKey,
) -> (OutPoint, TxOut) {
let script_pubkey = ScriptBuf::new_p2tr(secp, internal_key, None);
let out_point = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 0,
};
let utxo = TxOut { value: DUMMY_UTXO_AMOUNT, script_pubkey };
(out_point, utxo)
}
fn main() {
let secp = Secp256k1::new();
// Get a keypair we control. In a real application these would come from a stored secret.
let keypair = senders_keys(&secp);
let (internal_key, _parity) = keypair.x_only_public_key();
// Get an unspent output that is locked to the key above that we control.
// In a real application these would come from the chain.
let (dummy_out_point, dummy_utxo) = dummy_unspent_transaction_output(&secp, internal_key);
// Get an address to send to.
let address = receivers_address();
// The input for the transaction we are constructing.
let input = TxIn {
previous_output: dummy_out_point, // The dummy output we are spending.
script_sig: ScriptBuf::default(), // For a p2tr script_sig is empty.
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::default(), // Filled in after signing.
};
// The spend output is locked to a key controlled by the receiver.
let spend = TxOut { value: SPEND_AMOUNT, script_pubkey: address.script_pubkey() };
// The change output is locked to a key controlled by us.
let change = TxOut {
value: CHANGE_AMOUNT,
script_pubkey: ScriptBuf::new_p2tr(&secp, internal_key, None), // Change comes back to us.
};
// The transaction we want to sign and broadcast.
let mut unsigned_tx = Transaction {
version: transaction::Version::TWO, // Post BIP-68.
lock_time: absolute::LockTime::ZERO, // Ignore the locktime.
input: vec![input], // Input goes into index 0.
output: vec![spend, change], // Outputs, order does not matter.
};
let input_index = 0;
// Get the sighash to sign.
let sighash_type = TapSighashType::Default;
let prevouts = vec![dummy_utxo];
let prevouts = Prevouts::All(&prevouts);
let mut sighasher = SighashCache::new(&mut unsigned_tx);
let sighash = sighasher
.taproot_key_spend_signature_hash(input_index, &prevouts, sighash_type)
.expect("failed to construct sighash");
// Sign the sighash using the secp256k1 library (exported by rust-bitcoin).
let tweaked: TweakedKeypair = keypair.tap_tweak(&secp, None);
let msg = Message::from_digest(sighash.to_byte_array());
let signature = secp.sign_schnorr(&msg, &tweaked.to_inner());
// Update the witness stack.
let signature = bitcoin::taproot::Signature { signature, sighash_type };
sighasher.witness_mut(input_index).unwrap().push(&signature.to_vec());
// Get the signed transaction.
let tx = sighasher.into_transaction();
// BOOM! Transaction signed and ready to broadcast.
println!("{:#?}", tx);
}
Let's go over the main function code block by block.
let secp = Secp256k1::new();
creates a new Secp256k1
context with all capabilities.
Since we added the rand-std
feature to our Cargo.toml
,
we can use the SecretKey::new
method to generate a random private key sk
.
let keypair = senders_keys(&secp);
generates a keypair that we control,
and let (internal_key, _parity) = keypair.x_only_public_key();
generates a XOnlyPublicKey
that represent an X-only public key, used for verification of Schnorr signatures according to BIP340.
We won't be using second element from the returned tuple, the parity, so we are ignoring it by using the _
underscore.
let address = receivers_address();
generates a receiver's address address
.
let (dummy_out_point, dummy_utxo) = dummy_unspent_transaction_output(&secp, internal_key);
generates a dummy unspent transaction output dummy_utxo
and its corresponding outpoint dummy_out_point
.
All of these are helper functions that we defined earlier.
In let input = TxIn {...}
we are instantiating the input for the transaction we are constructing
Inside the TxIn
struct we are setting the following fields:
previous_output
is the outpoint of the dummy UTXO we are spending; it has theOutPoint
type.script_sig
is the script code required to spend an output; it has theScriptBuf
type. We are instantiating a new empty script withScriptBuf::new()
.sequence
is the sequence number; it has theSequence
type. We are using theENABLE_RBF_NO_LOCKTIME
constant.witness
is the witness stack; has theWitness
type. We are using thedefault
method to create an empty witness that will be filled in later after signing. This is possible becauseWitness
implements theDefault
trait.
In let spend = TxOut {...}
we are instantiating the spend output.
Inside the TxOut
struct we are setting the following fields:
value
is the amount we are assigning to be spendable by givenscript_pubkey
; it has theAmount
type. We are using theconst SPEND_AMOUNT
that we defined earlier.script_pubkey
is the script code required to spend a P2TR output; it is aScriptBuf
type. We are using thescript_pubkey
method to generate the script pubkey from the receivers address. This will lock the output to the receiver's address.
In let change = TxOut {...}
we are instantiating the change output.
It is very similar to the spend
output, but we are now using the const CHANGE_AMOUNT
that we defined earlier5.
This is done by setting the script_pubkey
field to ScriptBuf::new_p2tr(...)
,
which generates P2TR-type of script pubkey.
In let unsigned_tx = Transaction {...}
we are instantiating the transaction we want to sign and broadcast using the Transaction
struct.
We set the following fields:
version
is the transaction version; it has thetransaction::Version
type. We are using version2
which means that BIP68 applies.lock_time
is the transaction lock time; it is aLockTime
enum. We are using the constantZERO
This will make the transaction valid immediately.input
is the input vector; it is aVec<TxIn>
type. We are using theinput
variable that we defined earlier wrapped in thevec!
macro for convenient initialization.output
is the output vector; it is aVec<TxOut>
type. We are using thespend
andchange
variables that we defined earlier wrapped in thevec!
macro for convenient initialization.
We need to reference the outputs of previous transactions in our transaction.
We accomplish this with the Prevouts
enum.
In let prevouts = vec![dummy_utxo];
,
we create a vector of TxOut
types that we want to reference.
In our case, we only have one output, the dummy_utxo
that we defined earlier.
With let prevouts = Prevouts::All(&prevouts);
we create a Prevouts::All
variant that takes a reference to a vector of TxOut
types.
In let mut sighash_cache = SighashCache::new(unsigned_tx);
we are instantiating a SighashCache
struct.
This is a type that efficiently calculates signature hash message for legacy, segwit and taproot inputs.
We are using the new
method to instantiate the struct with the unsigned_tx
that we defined earlier.
new
takes any Borrow<Transaction>
as an argument.
Borrow<T>
is a trait that allows us to pass either a reference to a T
or a T
itself.
Hence, you can pass a Transaction
, a &Transaction
or a smart pointer to new
.
sighash_cache
is bound as mutable because we are updating it with computed values during signing.
This is reflected by taproot_signature_hash
taking a mutable reference.
This computes the BIP341 sighash for any flag type.
It takes the following arguments:
input_index
is the index of the input we are signing; it has theusize
type. We are using0
since we only have one input.&prevouts
is a reference to thePrevouts
enum that we defined earlier. This is used to reference the outputs of previous transactions and also used to calculate our transaction value.annex
is an optional argument that is used to pass the annex data. We are not using it, so we are passingNone
.leaf_hash_code_separator
is an optional argument that is used to pass the leaf hash code separator. We are not using it, so we are passingNone
.sighash_type
is the type of sighash; it is aTapSighashType
enum. We are using theAll
variant, which indicates that the sighash will include all the inputs and outputs.
Since Taproot outputs contain the tweaked key and keypair
represents untweaked (internal) key we have to tweak the key before signing using
let tweaked: TweakedKeypair = keypair.tap_tweak(&secp, None);
.
We create the message msg
by converting the sighash
to a Message
type.
This is a the message that we will sign.
The Message::from method is available for types that are intended and safe for signing.
We compute the signature sig
by using the sign_schnorr
method.
It takes a reference to a Message
and a reference to a Keypair
as arguments,
and returns a Signature
type.
In the next step, we update the witness stack for the input we just signed by first releasing the Transaction
from sighash_cache
by using the into_transaction
method.
We access the witness field of the first input with tx.input[0].witness
.
It is a Witness
type.
We use the push
method
to push the serialized public and private Taproot keys.
It expects a single argument of type AsRef<[u8]>
which is a reference to a byte slice.
We are using the as_ref
method to convert the signature sig
to a byte slice.
As the last step we print this to terminal using the println!
macro.
This transaction is now ready to be broadcast to the Bitcoin network.
Please note that the CHANGE_AMOUNT
is not the same as the DUMMY_UTXO_AMOUNT
minus the SPEND_AMOUNT
.
This is due to the fact that we need to pay a fee for the transaction.
We will be unwrapping any Option<T>
/Result<T, E>
with the expect
method.
Under the hood we are using the secp256k1
crate to generate the key pair.
rust-secp256k1
is a wrapper around libsecp256k1, a C
library implementing various cryptographic functions using the SECG curve
secp256k1.
this is an arbitrary mainnet address from block 805222.
And also we are locking the output to an address that we control:
the internal_key
public key hash that we generated earlier.