Introduction
This book is created and maintained by those involved in the
rust-bitcoin GitHub organization, contributions are
appreciated. It covers various crates from the org and as such, aims to be useful to developers
wanting to write code in Rust that interacts with the Bitcoin network. It is specifically not
limited to just the rust-bitcoin crate, although
that is a good starting point if you want a one-stop-shop for interacting with Bitcoin in Rust.
There are a number of good libraries outside of the rust-bitcoin organization that use the crates
covered here, two that you might like to check out are:
Finally, this book is currently a work in progress but hopes to eventually cover various topics, including parsing blocks and transactions, constructing and signing transactions, receiving data over the peer-to-peer network, plus fun stuff you can do with miniscript.
Table of Contents
License
This website is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication.
Getting Started
To add rust-bitcoin to your project, run:
cargo add bitcoin
If you are just exploring you probably want to use the "rand-std" feature so you can generate random keys:
cargo add bitcoin --features=rand-std
Constructing and Signing Transactions
We provide the following examples:
Constructing and Signing Transactions - SegWit V0
In this section, we will construct a SegWit V0 transaction. This is the most common type of transaction on the Bitcoin network today1.
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 std::str::FromStr;
use bitcoin::hashes::Hash;
use bitcoin::locktime::absolute;
use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing};
use bitcoin::sighash::{EcdsaSighashType, SighashCache};
use bitcoin::{
transaction, Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut,
Txid, WPubkeyHash, Witness,
};
Here is the logic behind these imports:
std::str::FromStris used to parse strings into Bitcoin primitivesbitcoin::hashes::Hashis used to hash databitcoin::locktime::absoluteis used to create a locktimebitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing}is used to sign transactionsbitcoin::sighash::{EcdsaSighashType, SighashCache}is used to create sighashesbitcoin::{Address, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, WPubkeyHash, 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_AMOUNTis the amount of the dummy UTXO we will be spendingSPEND_AMOUNTis the amount we will be spending from the dummy UTXOCHANGE_AMOUNT2 is the amount we will be sending back to ourselves as change
Before we can construct the transaction, we need to define some helper functions3:
use bitcoin::secp256k1::{rand, Secp256k1, SecretKey, Signing};
use bitcoin::WPubkeyHash;
fn senders_keys<C: Signing>(secp: &Secp256k1<C>) -> (SecretKey, WPubkeyHash) {
let sk = SecretKey::new(&mut rand::thread_rng());
let pk = bitcoin::PublicKey::new(sk.public_key(secp));
let wpkh = pk.wpubkey_hash().expect("key is compressed");
(sk, wpkh)
}
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 secrets4.
We use the SecretKey::new method to generate a random private key sk.
We then use the PublicKey::new method to derive the corresponding public key pk.
Finally, we use the PublicKey::wpubkey_hash method to derive the corresponding public key hash wpkh.
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.
We conclude returning the private key sk and the public key hash wpkh as a tuple.
use std::str::FromStr;
use bitcoin::{Address, Network};
fn receivers_address() -> Address {
Address::from_str("bc1q7cyrfmck2ffu2ud3rn5l5a8yv6f0chkp0zpemf")
.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 method Address::from_str to parse the string "bc1q7cyrfmck2ffu2ud3rn5l5a8yv6f0chkp0zpemf" into an address.
Hence, it is necessary to import the std::str::FromStr trait.
Note that bc1q7cyrfmck2ffu2ud3rn5l5a8yv6f0chkp0zpemf is a Bech32 address.
This is an arbitrary, however valid, Bitcoin mainnet address.
Hence we use the require_network method to ensure that the address is valid for mainnet.
use bitcoin::{Amount, OutPoint, ScriptBuf, TxOut, Txid, WPubkeyHash};
use bitcoin::hashes::Hash;
const DUMMY_UTXO_AMOUNT: Amount = Amount::from_sat(20_000_000);
fn dummy_unspent_transaction_output(wpkh: &WPubkeyHash) -> (OutPoint, TxOut) {
let script_pubkey = ScriptBuf::new_p2wpkh(wpkh);
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 SegWit V0 P2WPKH (ScriptBuf::new_p2wpkh) UTXO with a dummy invalid transaction ID (txid: Txid::all_zeros()),
and a value of the const DUMMY_UTXO_AMOUNT that we defined earlier.
We are using the OutPoint struct to represent the 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 p2wpkh unspent output:
use std::str::FromStr;
use bitcoin::hashes::Hash;
use bitcoin::locktime::absolute;
use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing};
use bitcoin::sighash::{EcdsaSighashType, SighashCache};
use bitcoin::{
transaction, Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut,
Txid, WPubkeyHash, 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>) -> (SecretKey, WPubkeyHash) {
let sk = SecretKey::new(&mut rand::thread_rng());
let pk = bitcoin::PublicKey::new(sk.public_key(secp));
let wpkh = pk.wpubkey_hash().expect("key is compressed");
(sk, wpkh)
}
fn receivers_address() -> Address {
Address::from_str("bc1q7cyrfmck2ffu2ud3rn5l5a8yv6f0chkp0zpemf")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
}
fn dummy_unspent_transaction_output(wpkh: &WPubkeyHash) -> (OutPoint, TxOut) {
let script_pubkey = ScriptBuf::new_p2wpkh(wpkh);
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 secret key we control and the pubkeyhash of the associated pubkey.
// In a real application these would come from a stored secret.
let (sk, wpkh) = senders_keys(&secp);
// Get an address to send to.
let address = receivers_address();
// 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(&wpkh);
// 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 p2wpkh 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_p2wpkh(&wpkh), // 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 = EcdsaSighashType::All;
let mut sighasher = SighashCache::new(&mut unsigned_tx);
let sighash = sighasher
.p2wpkh_signature_hash(input_index, &dummy_utxo.script_pubkey, DUMMY_UTXO_AMOUNT, sighash_type)
.expect("failed to create sighash");
// Sign the sighash using the secp256k1 library (exported by rust-bitcoin).
let msg = Message::from(sighash);
let signature = secp.sign_ecdsa(&msg, &sk);
// Update the witness stack.
let signature = bitcoin::ecdsa::Signature { signature, sighash_type };
let pk = sk.public_key(&secp);
*sighasher.witness_mut(input_index).unwrap() = Witness::p2wpkh(&signature, &pk);
// 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 (sk, wpkh) = senders_keys(&secp); generates a random private key sk and derives the corresponding public key hash wpkh.
let address = receivers_address(); generates a receiver's address address.
let (dummy_out_point, dummy_utxo) = dummy_unspent_transaction_output(&wpkh); 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.
let script_code = dummy_utxo.script_pubkey.p2wpkh_script_code().expect("valid script");
creates the script code required to spend a P2WPKH output.
Since dummy_utxo is a TxOut type,
we can access the underlying public field script_pubkey which, in turn is a Script type.
We then use the p2wpkh_script_code method to generate the script code.
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_outputis the outpoint of the dummy UTXO we are spending; it is aOutPointtype.script_sigis the script code required to spend a P2WPKH output; it is aScriptBuftype. It should be empty. That's why theScriptBuf::new().sequenceis the sequence number; it is aSequencetype. We are using theENABLE_RBF_NO_LOCKTIMEconstant.witnessis the witness stack; it is aWitnesstype. We are using thedefaultmethod to create an empty witness that will be filled in later after signing. This is possible becauseWitnessimplements theDefaulttrait.
In let spend = TxOut {...} we are instantiating the spend output.
Inside the TxOut struct we are setting the following fields:
valueis the amount we are spending; it is au64type. We are using theconst SPEND_AMOUNTthat we defined earlier.script_pubkeyis the script code required to spend a P2WPKH output; it is aScriptBuftype. We are using thescript_pubkeymethod 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_p2wpkh(&wpkh),
which generates P2WPKH-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:
versionis the transaction version; it is ai32type. We are using version2which means that BIP68 applies.lock_timeis the transaction lock time; it is aLockTimeenum. We are using the constantZEROThis will make the transaction valid immediately.inputis the input vector; it is aVec<TxIn>type. We are using theinputvariable that we defined earlier wrapped in thevec!macro for convenient initialization.outputis the output vector; it is aVec<TxOut>type. We are using thespendandchangevariables that we defined earlier wrapped in thevec!macro for convenient initialization.
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 or a &Transaction to new.
sighash_cache is instantiated as mutable because we require a mutable reference when creating the sighash to sign using segwit_signature_hash.
This computes the BIP143 sighash for any flag type.
It takes the following arguments:
input_indexis the index of the input we are signing; it is ausizetype. We are using0since we only have one input.script_codeis the script code required to spend a P2WPKH output; it is a reference toScripttype. We are using thescript_codevariable that we defined earlier.valueis the amount of the UTXO we are spending; it is au64type. We are using theconst DUMMY_UTXO_AMOUNTthat we defined earlier.sighash_typeis the type of sighash; it is aEcdsaSighashTypeenum. We are using theAllvariant, which indicates that the sighash will include all the inputs and outputs.
We create the message msg by converting the sighash to a Message type.
This is the message that we will sign.
The Message::from method takes anything that implements the promises to be a thirty two byte hash i.e., 32 bytes that came from a cryptographically secure hashing algorithm.
We compute the signature sig by using the sign_ecdsa method.
It takes a reference to a Message and a reference to a SecretKey as arguments,
and returns a Signature type.
In the next step, we update the witness stack for the input we just signed by first converting the sighash_cache into a Transaction
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_bitcoin_signature method.
It expects two arguments:
- A reference to a
SerializedSignaturetype. This is accomplished by calling theserialize_dermethod on theSignaturesig, which returns aSerializedSignaturetype. - A
EcdsaSighashTypeenum. Again we are using the sameAllvariant that we used earlier.
We repeat the same step as above, but now using the push method
to push the serialized public key to the witness stack.
It expects a single argument of type AsRef<[u8]> which is a reference 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.
-
mid-2023. ↩
-
Please note that the
CHANGE_AMOUNTis not the same as theDUMMY_UTXO_AMOUNTminus theSPEND_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 theexpectmethod. ↩ -
Under the hood we are using the
secp256k1crate to generate the key pair.rust-secp256k1is a wrapper around libsecp256k1, a C library implementing various cryptographic functions using the SECG curve secp256k1. ↩ -
And also we are locking the output to an address that we control: ↩
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::keyis used to tweak keys according to BIP340bitcoin::hashes::Hashis used to hash databitcoin::locktime::absoluteis 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_AMOUNTis the amount of the dummy UTXO we will be spendingSPEND_AMOUNTis the amount we will be spending from the dummy UTXOCHANGE_AMOUNT1 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:
secpis a reference to aSecp256k1type. This is used to verify the internal key.internal_keyis aUntweakedPublicKeytype. 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_rootis an optionalTapNodeHashtype. 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_outputis the outpoint of the dummy UTXO we are spending; it has theOutPointtype.script_sigis the script code required to spend an output; it has theScriptBuftype. We are instantiating a new empty script withScriptBuf::new().sequenceis the sequence number; it has theSequencetype. We are using theENABLE_RBF_NO_LOCKTIMEconstant.witnessis the witness stack; has theWitnesstype. We are using thedefaultmethod to create an empty witness that will be filled in later after signing. This is possible becauseWitnessimplements theDefaulttrait.
In let spend = TxOut {...} we are instantiating the spend output.
Inside the TxOut struct we are setting the following fields:
valueis the amount we are assigning to be spendable by givenscript_pubkey; it has theAmounttype. We are using theconst SPEND_AMOUNTthat we defined earlier.script_pubkeyis the script code required to spend a P2TR output; it is aScriptBuftype. We are using thescript_pubkeymethod 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:
versionis the transaction version; it has thetransaction::Versiontype. We are using version2which means that BIP68 applies.lock_timeis the transaction lock time; it is aLockTimeenum. We are using the constantZEROThis will make the transaction valid immediately.inputis the input vector; it is aVec<TxIn>type. We are using theinputvariable that we defined earlier wrapped in thevec!macro for convenient initialization.outputis the output vector; it is aVec<TxOut>type. We are using thespendandchangevariables 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_indexis the index of the input we are signing; it has theusizetype. We are using0since we only have one input.&prevoutsis a reference to thePrevoutsenum that we defined earlier. This is used to reference the outputs of previous transactions and also used to calculate our transaction value.annexis an optional argument that is used to pass the annex data. We are not using it, so we are passingNone.leaf_hash_code_separatoris an optional argument that is used to pass the leaf hash code separator. We are not using it, so we are passingNone.sighash_typeis the type of sighash; it is aTapSighashTypeenum. We are using theAllvariant, 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_AMOUNTis not the same as theDUMMY_UTXO_AMOUNTminus theSPEND_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 theexpectmethod. ↩ -
Under the hood we are using the
secp256k1crate to generate the key pair.rust-secp256k1is 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_keypublic key hash that we generated earlier. ↩
Working with PSBTs
The Partially Signed Bitcoin Transaction (PSBT) format specifies an encoding for partially signed transactions. PSBTs are used in the context of multisignature wallets, hardware wallets, and other use cases where multiple parties need to collaborate to sign a transaction.
PSBT version 0 is defined in BIP 174. It specifies 6 different roles that a party can play in the PSBT workflow:
- Creator: Creates the PSBT and adds inputs and outputs.
- Updater: Adds additional information to the PSBT,
such as
redeemScript,witnessScript, and BIP32 derivation paths. - Signer: Signs the PSBT, either all inputs or a subset of them.
- Combiner: Combines multiple PSBTs into a single PSBT.
- Finalizer: Finalizes the PSBT, adding any information necessary to complete the transaction.
- Extractor: Extracts the finalized transaction from the PSBT.
Note that multiple roles can be handled by a single entity but each role is specialized in what it should be capable of doing.
We provide the following examples:
- Constructing and Signing Multiple Inputs - SegWit V0
- Constructing and Signing Multiple Inputs - Taproot
For extra information, see the Bitcoin Optech article on PSBTs.
PSBTs: Constructing and Signing Multiple Inputs - SegWit V0
The purpose of this section is to construct a PSBT that spends multiple inputs and signs it. We'll cover the following BIP 174 roles:
- Creator: Creates a PSBT with multiple inputs and outputs.
- Updater: Adds Witness and SegWit V0 data to the PSBT.
- Signer: Signs the PSBT.
- Finalizer: Finalizes the PSBT.
The example will focus on spending two SegWit V0 inputs:
- 20,000,000 satoshi UTXO, the first receiving ("external") address.
- 10,000,000 satoshi UTXO, the first change ("internal") address.
We'll be sending this to two outputs:
- 25,000,000 satoshis to a receivers' address.
- 4,990,000 satoshis back to us as change.
The miner's fee will be 10,000 satoshis.
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 std::collections::BTreeMap;
use std::str::FromStr;
use bitcoin::bip32::{ChildNumber, Fingerprint, IntoDerivationPath as _, Xpriv, Xpub};
use bitcoin::hashes::Hash;
use bitcoin::locktime::absolute;
use bitcoin::secp256k1::{Secp256k1, Signing};
use bitcoin::{
consensus, psbt, transaction, Address, Amount, EcdsaSighashType, Network, OutPoint, Psbt,
ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, WPubkeyHash, Witness,
};
Here is the logic behind these imports:
std::collections::BTreeMapis used to store the key-value pairs of the Public Key PSBT input fields.std::str::FromStris used to parse strings into Bitcoin primitivesbitcoin::bip32is used to derive keys according to BIP 32bitcoin::hashes::Hashis used to hash databitcoin::locktime::absoluteis used to create a locktimebitcoin::secp256k1is used to sign transactionsbitcoin::consensusis used to serialize the final signed transaction to a raw transactionbitcoin::psbtis used to construct and manipulate PSBTsbitcoin::transactionandbitcoin::{Address, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness}are used to construct transactionsbitcoin::WPubkeyHashis used to construct SegWit V0 inputs
Next, we define the following constants:
use bitcoin::Amount;
const XPRIV: &str = "xprv9tuogRdb5YTgcL3P8Waj7REqDuQx4sXcodQaWTtEVFEp6yRKh1CjrWfXChnhgHeLDuXxo2auDZegMiVMGGxwxcrb2PmiGyCngLxvLeGsZRq";
const BIP84_DERIVATION_PATH: &str = "m/84'/0'/0'";
const MASTER_FINGERPRINT: &str = "9680603f";
const DUMMY_UTXO_AMOUNT_INPUT_1: Amount = Amount::from_sat(20_000_000);
const DUMMY_UTXO_AMOUNT_INPUT_2: Amount = Amount::from_sat(10_000_000);
const SPEND_AMOUNT: Amount = Amount::from_sat(25_000_000);
const CHANGE_AMOUNT: Amount = Amount::from_sat(4_990_000); // 10_000 sat fee.
XPRIVis the extended private key that will be used to derive the keys for the SegWit V0 inputs.MASTER_FINGERPRINTis the fingerprint of the master key.BIP84_DERIVATION_PATHis the derivation path for the BIP 84 key. Since this is a mainnet example, we are using the pathm/84'/0'/0'.DUMMY_UTXO_AMOUNT_INPUT_1is the amount of the dummy UTXO we will be spending from the first input.DUMMY_UTXO_AMOUNT_INPUT_2is the amount of the dummy UTXO we will be spending from the second input.SPEND_AMOUNTis the amount we will be spending from the dummy UTXO related to the first input.CHANGE_AMOUNT1 is the amount we will be sending back to ourselves as change.
Before we can construct the transaction, we need to define some helper functions:
use std::collections::BTreeMap;
use std::str::FromStr;
use bitcoin::bip32::{ChildNumber, IntoDerivationPath, Fingerprint, Xpriv, Xpub};
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::{Secp256k1, Signing};
use bitcoin::{Address, Amount, Network, OutPoint, Txid, TxOut};
const BIP84_DERIVATION_PATH: &str = "m/84'/0'/0'";
const DUMMY_UTXO_AMOUNT_INPUT_1: Amount = Amount::from_sat(20_000_000);
const DUMMY_UTXO_AMOUNT_INPUT_2: Amount = Amount::from_sat(10_000_000);
fn get_external_address_xpriv<C: Signing>(
secp: &Secp256k1<C>,
master_xpriv: Xpriv,
index: u32,
) -> Xpriv {
let derivation_path =
BIP84_DERIVATION_PATH.into_derivation_path().expect("valid derivation path");
let child_xpriv = master_xpriv
.derive_priv(secp, &derivation_path)
.expect("valid child xpriv");
let external_index = ChildNumber::from_normal_idx(0).unwrap();
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv
.derive_priv(secp, &[external_index, idx])
.expect("valid xpriv")
}
fn get_internal_address_xpriv<C: Signing>(
secp: &Secp256k1<C>,
master_xpriv: Xpriv,
index: u32,
) -> Xpriv {
let derivation_path =
BIP84_DERIVATION_PATH.into_derivation_path().expect("valid derivation path");
let child_xpriv = master_xpriv
.derive_priv(secp, &derivation_path)
.expect("valid child xpriv");
let internal_index = ChildNumber::from_normal_idx(1).unwrap();
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv
.derive_priv(secp, &[internal_index, idx])
.expect("valid xpriv")
}
fn receivers_address() -> Address {
Address::from_str("bc1q7cyrfmck2ffu2ud3rn5l5a8yv6f0chkp0zpemf")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
}
fn dummy_unspent_transaction_outputs() -> Vec<(OutPoint, TxOut)> {
let script_pubkey_1 = Address::from_str("bc1qrwuu3ydv0jfza4a0ehtfd03m9l4vw3fy0hfm50")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
.script_pubkey();
let out_point_1 = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 0,
};
let utxo_1 = TxOut {
value: DUMMY_UTXO_AMOUNT_INPUT_1,
script_pubkey: script_pubkey_1,
};
let script_pubkey_2 = Address::from_str("bc1qy7swwpejlw7a2rp774pa8rymh8tw3xvd2x2xkd")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
.script_pubkey();
let out_point_2 = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 1,
};
let utxo_2 = TxOut {
value: DUMMY_UTXO_AMOUNT_INPUT_2,
script_pubkey: script_pubkey_2,
};
vec![(out_point_1, utxo_1), (out_point_2, utxo_2)]
}
get_external_address_xpriv and get_internal_address_xpriv generates the external and internal addresses extended private key,
given a master extended private key and an address index; respectively.
Note that these functions takes a Secp256k1 that is
generic over the Signing trait.
This is used to indicate that is an instance of Secp256k1 and can be used for signing and other things.
receivers_address generates a receiver address.
In a real application this would be the address of the receiver.
We use the method Address::from_str to parse the string of addresses2 into an Address.
Hence, it is necessary to import the std::str::FromStr trait.
This is an arbitrary, however valid, Bitcoin mainnet address.
Hence we use the require_network method to ensure that the address is valid for mainnet.
dummy_unspent_transaction_outputs generates a dummy unspent transaction output (UTXO).
This is a P2WPKH (ScriptBuf::new_p2wpkh) UTXO.
The UTXO has a dummy invalid transaction ID (txid: Txid::all_zeros()),
and any value of the const DUMMY_UTXO_AMOUNT_N that we defined earlier.
Note that the vout is set to 0 for the first UTXO and 1 for the second UTXO.
We are using the OutPoint struct to represent the transaction output.
Finally, we return vector of tuples (out_point, utxo).
Now we are ready for our main function that will create, update, and sign a PSBT;
while also extracting a transaction that spends the p2wpkhs unspent outputs:
use std::collections::BTreeMap;
use std::str::FromStr;
use bitcoin::bip32::{ChildNumber, Fingerprint, IntoDerivationPath as _, Xpriv, Xpub};
use bitcoin::hashes::Hash;
use bitcoin::locktime::absolute;
use bitcoin::secp256k1::{Secp256k1, Signing};
use bitcoin::{
consensus, psbt, transaction, Address, Amount, EcdsaSighashType, Network, OutPoint, Psbt,
ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, WPubkeyHash, Witness,
};
const XPRIV: &str = "xprv9tuogRdb5YTgcL3P8Waj7REqDuQx4sXcodQaWTtEVFEp6yRKh1CjrWfXChnhgHeLDuXxo2auDZegMiVMGGxwxcrb2PmiGyCngLxvLeGsZRq";
const BIP84_DERIVATION_PATH: &str = "m/84'/0'/0'";
const MASTER_FINGERPRINT: &str = "9680603f";
const DUMMY_UTXO_AMOUNT_INPUT_1: Amount = Amount::from_sat(20_000_000);
const DUMMY_UTXO_AMOUNT_INPUT_2: Amount = Amount::from_sat(10_000_000);
const SPEND_AMOUNT: Amount = Amount::from_sat(25_000_000);
const CHANGE_AMOUNT: Amount = Amount::from_sat(4_990_000); // 10_000 sat fee.
fn get_external_address_xpriv<C: Signing>(
secp: &Secp256k1<C>,
master_xpriv: Xpriv,
index: u32,
) -> Xpriv {
let derivation_path =
BIP84_DERIVATION_PATH.into_derivation_path().expect("valid derivation path");
let child_xpriv = master_xpriv
.derive_priv(secp, &derivation_path)
.expect("valid child xpriv");
let external_index = ChildNumber::from_normal_idx(0).unwrap();
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv
.derive_priv(secp, &[external_index, idx])
.expect("valid xpriv")
}
fn get_internal_address_xpriv<C: Signing>(
secp: &Secp256k1<C>,
master_xpriv: Xpriv,
index: u32,
) -> Xpriv {
let derivation_path =
BIP84_DERIVATION_PATH.into_derivation_path().expect("valid derivation path");
let child_xpriv = master_xpriv
.derive_priv(secp, &derivation_path)
.expect("valid child xpriv");
let internal_index = ChildNumber::from_normal_idx(1).unwrap();
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv
.derive_priv(secp, &[internal_index, idx])
.expect("valid xpriv")
}
fn receivers_address() -> Address {
Address::from_str("bc1q7cyrfmck2ffu2ud3rn5l5a8yv6f0chkp0zpemf")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
}
fn dummy_unspent_transaction_outputs() -> Vec<(OutPoint, TxOut)> {
let script_pubkey_1 = Address::from_str("bc1qrwuu3ydv0jfza4a0ehtfd03m9l4vw3fy0hfm50")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
.script_pubkey();
let out_point_1 = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 0,
};
let utxo_1 = TxOut {
value: DUMMY_UTXO_AMOUNT_INPUT_1,
script_pubkey: script_pubkey_1,
};
let script_pubkey_2 = Address::from_str("bc1qy7swwpejlw7a2rp774pa8rymh8tw3xvd2x2xkd")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
.script_pubkey();
let out_point_2 = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 1,
};
let utxo_2 = TxOut {
value: DUMMY_UTXO_AMOUNT_INPUT_2,
script_pubkey: script_pubkey_2,
};
vec![(out_point_1, utxo_1), (out_point_2, utxo_2)]
}
fn main() {
let secp = Secp256k1::new();
// Get the individual xprivs we control. In a real application these would come from a stored secret.
let master_xpriv = XPRIV.parse::<Xpriv>().expect("valid xpriv");
let xpriv_input_1 = get_external_address_xpriv(&secp, master_xpriv, 0);
let xpriv_input_2 = get_internal_address_xpriv(&secp, master_xpriv, 0);
let xpriv_change = get_internal_address_xpriv(&secp, master_xpriv, 1);
// Get the PKs
let pk_input_1 = Xpub::from_priv(&secp, &xpriv_input_1).to_pub();
let pk_input_2 = Xpub::from_priv(&secp, &xpriv_input_2).to_pub();
let pk_inputs = [pk_input_1, pk_input_2];
let pk_change = Xpub::from_priv(&secp, &xpriv_change).to_pub();
// Get the Witness Public Key Hashes (WPKHs)
let wpkhs: Vec<WPubkeyHash> = pk_inputs.iter().map(|pk| pk.wpubkey_hash()).collect();
// Get the unspent outputs that are locked to the key above that we control.
// In a real application these would come from the chain.
let utxos: Vec<TxOut> = dummy_unspent_transaction_outputs()
.into_iter()
.map(|(_, utxo)| utxo)
.collect();
// Get the addresses to send to.
let address = receivers_address();
// The inputs for the transaction we are constructing.
let inputs: Vec<TxIn> = dummy_unspent_transaction_outputs()
.into_iter()
.map(|(outpoint, _)| TxIn {
previous_output: outpoint,
script_sig: ScriptBuf::default(),
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::default(),
})
.collect();
// 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_p2wpkh(&pk_change.wpubkey_hash()), // Change comes back to us.
};
// The transaction we want to sign and broadcast.
let unsigned_tx = Transaction {
version: transaction::Version::TWO, // Post BIP 68.
lock_time: absolute::LockTime::ZERO, // Ignore the locktime.
input: inputs, // Input is 0-indexed.
output: vec![spend, change], // Outputs, order does not matter.
};
// Now we'll start the PSBT workflow.
// Step 1: Creator role; that creates,
// and add inputs and outputs to the PSBT.
let mut psbt = Psbt::from_unsigned_tx(unsigned_tx).expect("Could not create PSBT");
// Step 2:Updater role; that adds additional
// information to the PSBT.
let ty = EcdsaSighashType::All.into();
let derivation_paths = [
"m/84'/0'/0'/0/0".into_derivation_path().expect("valid derivation path"), // First external address.
"m/84'/0'/0'/1/0".into_derivation_path().expect("valid derivation path"), // First internal address.
];
let mut bip32_derivations = Vec::new();
for (idx, pk) in pk_inputs.iter().enumerate() {
let mut map = BTreeMap::new();
let fingerprint = Fingerprint::from_str(MASTER_FINGERPRINT).expect("valid fingerprint");
map.insert(pk.0, (fingerprint, derivation_paths[idx].clone()));
bip32_derivations.push(map);
}
psbt.inputs = vec![
psbt::Input {
witness_utxo: Some(utxos[0].clone()),
redeem_script: Some(ScriptBuf::new_p2wpkh(&wpkhs[0])),
bip32_derivation: bip32_derivations[0].clone(),
sighash_type: Some(ty),
..Default::default()
},
psbt::Input {
witness_utxo: Some(utxos[1].clone()),
redeem_script: Some(ScriptBuf::new_p2wpkh(&wpkhs[1])),
bip32_derivation: bip32_derivations[1].clone(),
sighash_type: Some(ty),
..Default::default()
},
];
// Step 3: Signer role; that signs the PSBT.
psbt.sign(&master_xpriv, &secp).expect("valid signature");
// Step 4: Finalizer role; that finalizes the PSBT.
println!("PSBT Inputs: {:#?}", psbt.inputs);
let final_script_witness: Vec<_> = psbt
.inputs
.iter()
.enumerate()
.map(|(idx, input)| {
let (_, sig) = input.partial_sigs.iter().next().expect("we have one sig");
Witness::p2wpkh(sig, &pk_inputs[idx].0)
})
.collect();
psbt.inputs.iter_mut().enumerate().for_each(|(idx, input)| {
// Clear all the data fields as per the spec.
input.final_script_witness = Some(final_script_witness[idx].clone());
input.partial_sigs = BTreeMap::new();
input.sighash_type = None;
input.redeem_script = None;
input.witness_script = None;
input.bip32_derivation = BTreeMap::new();
});
// BOOM! Transaction signed and ready to broadcast.
let signed_tx = psbt.extract_tx().expect("valid transaction");
let serialized_signed_tx = consensus::encode::serialize_hex(&signed_tx);
println!("Transaction Details: {:#?}", signed_tx);
// check with:
// bitcoin-cli decoderawtransaction <RAW_TX> true
println!("Raw Transaction: {}", serialized_signed_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,
Next, we get the individual extended private keys (xpriv) that we control. These are:
- the master xpriv,
- the xprivs for inputs 1 and 2;
these are done with the
get_external_address_xprivandget_internal_address_xprivfunctions. - the xpriv for the change output, also using the
get_internal_address_xprivfunction.
The inputs for the transaction we are constructing,
here named utxos,
are created with the dummy_unspent_transaction_outputs function.
let address = receivers_address(); generates a receiver's address address.
All of these are helper functions that we defined earlier.
In let input = TxIn {...} we are instantiating the inputs for the transaction we are constructing
Inside the TxIn struct we are setting the following fields:
previous_outputis the outpoint of the dummy UTXO we are spending; it is aOutPointtype.script_sigis the script code required to spend an output; it is aScriptBuftype. We are instantiating a new empty script withScriptBuf::new().sequenceis the sequence number; it is aSequencetype. We are using theENABLE_RBF_NO_LOCKTIMEconstant.witnessis the witness stack; it is aWitnesstype. We are using thedefaultmethod to create an empty witness that will be filled in later after signing. This is possible becauseWitnessimplements theDefaulttrait.
In let spend = TxOut {...} we are instantiating the spend output.
Inside the TxOut struct we are setting the following fields:
valueis the amount we are spending; it is au64type. We are using theconst SPEND_AMOUNTthat we defined earlier.script_pubkeyis the script code required to spend a P2WPKH output; it is aScriptBuftype. We are using thescript_pubkeymethod 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 earlier3.
This is done by setting the script_pubkey field to ScriptBuf::new_p2wpkh(...),
which generates P2WPKH-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:
versionis the transaction version; it can be ai32type. However it is best to use theVersionstruct. We are using versionTWOwhich means that BIP 68 applies.lock_timeis the transaction lock time; it is aLockTimeenum. We are using the constantZEROThis will make the transaction valid immediately.inputis the input vector; it is aVec<TxIn>type. We are using theinputvariable that we defined earlier wrapped in thevec!macro for convenient initialization.outputis the output vector; it is aVec<TxOut>type. We are using thespendandchangevariables that we defined earlier wrapped in thevec!macro for convenient initialization.
Now we are ready to start our PSBT workflow.
The first step is the Creator role.
We create a PSBT from the unsigned transaction using the Psbt::from_unsigned_tx method.
Next, we move to the Updater role.
We add additional information to the PSBT.
This is done by setting the psbt.inputs field to a vector of Input structs.
In particular, we set the following fields:
witness_utxois the witness UTXO; it is anOption<TxOut>type. We are using theutxosvector that we defined earlier.redeem_scriptis the redeem script; it is anOption<ScriptBuf>type. We are using theScriptBuf::new_p2wpkhmethod to create a P2WPKH script.bip32_derivationis the BIP 32 derivation; it is aBTreeMap<Xpub, (Fingerprint, DerivationPath)>type. We are using a vector ofBTreeMaps.sighash_typeis the sighash type; it is anOption<PsbtSighashType>type.
All the other fields are set to their default values using the Default::default() method.
The following step is the Signer role.
Here is were we sign the PSBT with the
sign method.
This method takes the master extended private key and the Secp256k1 context as arguments.
It attempts to create all the required signatures for this PSBT using the extended private key.
Finally, we move to the Finalizer role. Here we finalize the PSBT, making it ready to be extracted into a signed transaction, and if necessary, broadcasted to the Bitcoin network. This is done by setting the following fields:
final_script_witnessis the final script witness; it is anOption<Witness>type. We are using theWitness::p2wpkh()method to create a witness required to spend a P2WPKH output.partial_sigsis the partial signatures; it is aBTreeMap<XOnlyPublicKey, Vec<u8>>type. We are using an empty map.sighash_typeis the sighash type; it is anOption<PsbtSighashType>type. We are using theNonevalue.redeem_scriptis the redeem script; it is anOption<ScriptBuf>type. We are using theNonevalue.witness_scriptis the witness script; it is anOption<ScriptBuf>type.bip32_derivationis the BIP 32 derivation; it is aBTreeMap<Xpub, (Fingerprint, DerivationPath)>type. We are using an empty map.
Finally, we extract the signed transaction from the PSBT using the extract_tx method.
As the last step we print both the transaction details and the raw transaction
to the terminal using the println! macro.
This transaction is now ready to be broadcast to the Bitcoin network.
For anything in production, the step 4 (Finalizer) should be done with the
psbt::PsbtExt from the miniscript crate trait.
It provides a
.finalize_mut
to a Psbt object,
which takes in a mutable reference to Psbt and populates the final_witness and final_scriptsig for all inputs.
-
Please note that the
CHANGE_AMOUNTis not the same as theDUMMY_UTXO_AMOUNT_INPUT_Ns minus theSPEND_AMOUNT. This is due to the fact that we need to pay a miner's fee for the transaction. ↩ -
this is an arbitrary mainnet addresses from block 805222. ↩
-
And also we are locking the output to an address that we control: the
wpkhpublic key hash that we generated earlier. ↩
PSBTs: Constructing and Signing Multiple Inputs - Taproot
The purpose of this section is to construct a PSBT that spends multiple inputs and signs it. We'll cover the following BIP 174 roles:
- Creator: Creates a PSBT with multiple inputs and outputs.
- Updater: Adds Witness and Taproot data to the PSBT.
- Signer: Signs the PSBT.
- Finalizer: Finalizes the PSBT.
The example will focus on spending two Taproot inputs:
- 20,000,000 satoshi UTXO, the first receiving ("external") address.
- 10,000,000 satoshi UTXO, the first change ("internal") address.
We'll be sending this to two outputs:
- 25,000,000 satoshis to a receivers' address.
- 4,990,000 satoshis back to us as change.
The miner's fee will be 10,000 satoshis.
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 std::collections::BTreeMap;
use std::str::FromStr;
use bitcoin::bip32::{ChildNumber, IntoDerivationPath, DerivationPath, Fingerprint, Xpriv, Xpub};
use bitcoin::hashes::Hash;
use bitcoin::key::UntweakedPublicKey;
use bitcoin::locktime::absolute;
use bitcoin::psbt::Input;
use bitcoin::secp256k1::{Secp256k1, Signing};
use bitcoin::{
consensus, transaction, Address, Amount, Network, OutPoint, Psbt, ScriptBuf, Sequence,
TapLeafHash, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness, XOnlyPublicKey,
};
Here is the logic behind these imports:
std::collections::BTreeMapis used to store the key-value pairs of the Tap Key origins PSBT input fields.std::str::FromStris used to parse strings into Bitcoin primitivesbitcoin::bip32is used to derive keys according to BIP 32bitcoin::hashes::Hashis used to hash databitcoin::keyis used to tweak keys according to BIP 340bitcoin::locktime::absoluteis used to create a locktimebitcoin::psbtis used to construct and manipulate PSBTsbitcoin::secp256k1is used to sign transactionsbitcoin::consensusis used to serialize the final signed transaction to a raw transactionbitcoin::transactionandbitcoin::{Address, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness}are used to construct transactionsbitcoin::{TapLeafHash, XOnlyPublicKey}is used to construct Taproot inputs
Next, we define the following constants:
use bitcoin::Amount;
const XPRIV: &str = "xprv9tuogRdb5YTgcL3P8Waj7REqDuQx4sXcodQaWTtEVFEp6yRKh1CjrWfXChnhgHeLDuXxo2auDZegMiVMGGxwxcrb2PmiGyCngLxvLeGsZRq";
const BIP86_DERIVATION_PATH: &str = "m/86'/0'/0'";
const MASTER_FINGERPRINT: &str = "9680603f";
const DUMMY_UTXO_AMOUNT_INPUT_1: Amount = Amount::from_sat(20_000_000);
const DUMMY_UTXO_AMOUNT_INPUT_2: Amount = Amount::from_sat(10_000_000);
const SPEND_AMOUNT: Amount = Amount::from_sat(25_000_000);
const CHANGE_AMOUNT: Amount = Amount::from_sat(4_990_000); // 10_000 sat fee.
XPRIVis the extended private key that will be used to derive the keys for the Taproot inputs.MASTER_FINGERPRINTis the fingerprint of the master key.BIP86_DERIVATION_PATHis the derivation path for the BIP 86 key. Since this is a mainnet example, we are using the pathm/86'/0'/0'.DUMMY_UTXO_AMOUNT_INPUT_1is the amount of the dummy UTXO we will be spending from the first input.DUMMY_UTXO_AMOUNT_INPUT_2is the amount of the dummy UTXO we will be spending from the second input.SPEND_AMOUNTis the amount we will be spending from the dummy UTXO related to the first input.CHANGE_AMOUNT1 is the amount we will be sending back to ourselves as change.
Before we can construct the transaction, we need to define some helper functions:
use std::collections::BTreeMap;
use std::str::FromStr;
use bitcoin::bip32::{ChildNumber, IntoDerivationPath, DerivationPath, Fingerprint, Xpriv, Xpub};
use bitcoin::hashes::Hash;
use bitcoin::key::UntweakedPublicKey;
use bitcoin::secp256k1::{Secp256k1, Signing};
use bitcoin::{Address, Amount, Network, OutPoint, TapLeafHash, Txid, TxOut, XOnlyPublicKey};
const BIP86_DERIVATION_PATH: &str = "m/86'/0'/0'";
const DUMMY_UTXO_AMOUNT_INPUT_1: Amount = Amount::from_sat(20_000_000);
const DUMMY_UTXO_AMOUNT_INPUT_2: Amount = Amount::from_sat(10_000_000);
fn get_external_address_xpriv<C: Signing>(
secp: &Secp256k1<C>,
master_xpriv: Xpriv,
index: u32,
) -> Xpriv {
let derivation_path =
BIP86_DERIVATION_PATH.into_derivation_path().expect("valid derivation path");
let child_xpriv = master_xpriv
.derive_priv(secp, &derivation_path)
.expect("valid child xpriv");
let external_index = ChildNumber::from_normal_idx(0).unwrap();
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv
.derive_priv(secp, &[external_index, idx])
.expect("valid xpriv")
}
fn get_internal_address_xpriv<C: Signing>(
secp: &Secp256k1<C>,
master_xpriv: Xpriv,
index: u32,
) -> Xpriv {
let derivation_path =
BIP86_DERIVATION_PATH.into_derivation_path().expect("valid derivation path");
let child_xpriv = master_xpriv
.derive_priv(secp, &derivation_path)
.expect("valid child xpriv");
let internal_index = ChildNumber::from_normal_idx(1).unwrap();
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv
.derive_priv(secp, &[internal_index, idx])
.expect("valid xpriv")
}
fn get_tap_key_origin(
x_only_key: UntweakedPublicKey,
master_fingerprint: Fingerprint,
path: DerivationPath,
) -> BTreeMap<XOnlyPublicKey, (Vec<TapLeafHash>, (Fingerprint, DerivationPath))> {
let mut map = BTreeMap::new();
map.insert(x_only_key, (vec![], (master_fingerprint, path)));
map
}
fn receivers_address() -> Address {
Address::from_str("bc1p0dq0tzg2r780hldthn5mrznmpxsxc0jux5f20fwj0z3wqxxk6fpqm7q0va")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
}
fn dummy_unspent_transaction_outputs() -> Vec<(OutPoint, TxOut)> {
let script_pubkey_1 =
Address::from_str("bc1p80lanj0xee8q667aqcnn0xchlykllfsz3gu5skfv9vjsytaujmdqtv52vu")
.unwrap()
.require_network(Network::Bitcoin)
.unwrap()
.script_pubkey();
let out_point_1 = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 0,
};
let utxo_1 = TxOut {
value: DUMMY_UTXO_AMOUNT_INPUT_1,
script_pubkey: script_pubkey_1,
};
let script_pubkey_2 =
Address::from_str("bc1pfd0jmmdnp278vppcw68tkkmquxtq50xchy7f6wdmjtjm7fgsr8dszdcqce")
.unwrap()
.require_network(Network::Bitcoin)
.unwrap()
.script_pubkey();
let out_point_2 = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 1,
};
let utxo_2 = TxOut {
value: DUMMY_UTXO_AMOUNT_INPUT_2,
script_pubkey: script_pubkey_2,
};
vec![(out_point_1, utxo_1), (out_point_2, utxo_2)]
}
get_external_address_xpriv and get_internal_address_xpriv generates the external and internal addresses extended private key,
given a master extended private key and an address index; respectively.
Note that these functions takes a Secp256k1 that is
generic over the Signing trait.
This is used to indicate that is an instance of Secp256k1 and can be used for signing and other things.
The get_tap_key_origin function generates a Tap Key Origin key-value map,
which is a map of Taproot X-only keys to origin info and leaf hashes contained in it.
This is necessary to sign a Taproot input.
receivers_address generates a receiver address.
In a real application this would be the address of the receiver.
We use the method Address::from_str to parse the string of addresses2 into an Address.
Hence, it is necessary to import the std::str::FromStr trait.
This is an arbitrary, however valid, Bitcoin mainnet address.
Hence we use the require_network method to ensure that the address is valid for mainnet.
dummy_unspent_transaction_outputs generates a dummy unspent transaction output (UTXO).
This is a P2TR (ScriptBuf::new_p2tr) UTXO.
The UTXO has a dummy invalid transaction ID (txid: Txid::all_zeros()),
and any value of the const DUMMY_UTXO_AMOUNT_N that we defined earlier.
Note that the vout is set to 0 for the first UTXO and 1 for the second UTXO.
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 transaction output.
Finally, we return vector of tuples (out_point, utxo).
Now we are ready for our main function that will create, update, and sign a PSBT;
while also extracting a transaction that spends the p2trs unspent outputs:
use std::collections::BTreeMap;
use std::str::FromStr;
use bitcoin::bip32::{ChildNumber, IntoDerivationPath, DerivationPath, Fingerprint, Xpriv, Xpub};
use bitcoin::hashes::Hash;
use bitcoin::key::UntweakedPublicKey;
use bitcoin::locktime::absolute;
use bitcoin::psbt::Input;
use bitcoin::secp256k1::{Secp256k1, Signing};
use bitcoin::{
consensus, transaction, Address, Amount, Network, OutPoint, Psbt, ScriptBuf, Sequence,
TapLeafHash, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness, XOnlyPublicKey,
};
const XPRIV: &str = "xprv9tuogRdb5YTgcL3P8Waj7REqDuQx4sXcodQaWTtEVFEp6yRKh1CjrWfXChnhgHeLDuXxo2auDZegMiVMGGxwxcrb2PmiGyCngLxvLeGsZRq";
const BIP86_DERIVATION_PATH: &str = "m/86'/0'/0'";
const MASTER_FINGERPRINT: &str = "9680603f";
const DUMMY_UTXO_AMOUNT_INPUT_1: Amount = Amount::from_sat(20_000_000);
const DUMMY_UTXO_AMOUNT_INPUT_2: Amount = Amount::from_sat(10_000_000);
const SPEND_AMOUNT: Amount = Amount::from_sat(25_000_000);
const CHANGE_AMOUNT: Amount = Amount::from_sat(4_990_000); // 10_000 sat fee.
fn get_external_address_xpriv<C: Signing>(
secp: &Secp256k1<C>,
master_xpriv: Xpriv,
index: u32,
) -> Xpriv {
let derivation_path =
BIP86_DERIVATION_PATH.into_derivation_path().expect("valid derivation path");
let child_xpriv = master_xpriv
.derive_priv(secp, &derivation_path)
.expect("valid child xpriv");
let external_index = ChildNumber::from_normal_idx(0).unwrap();
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv
.derive_priv(secp, &[external_index, idx])
.expect("valid xpriv")
}
fn get_internal_address_xpriv<C: Signing>(
secp: &Secp256k1<C>,
master_xpriv: Xpriv,
index: u32,
) -> Xpriv {
let derivation_path =
BIP86_DERIVATION_PATH.into_derivation_path().expect("valid derivation path");
let child_xpriv = master_xpriv
.derive_priv(secp, &derivation_path)
.expect("valid child xpriv");
let internal_index = ChildNumber::from_normal_idx(1).unwrap();
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv
.derive_priv(secp, &[internal_index, idx])
.expect("valid xpriv")
}
fn get_tap_key_origin(
x_only_key: UntweakedPublicKey,
master_fingerprint: Fingerprint,
path: DerivationPath,
) -> BTreeMap<XOnlyPublicKey, (Vec<TapLeafHash>, (Fingerprint, DerivationPath))> {
let mut map = BTreeMap::new();
map.insert(x_only_key, (vec![], (master_fingerprint, path)));
map
}
fn receivers_address() -> Address {
Address::from_str("bc1p0dq0tzg2r780hldthn5mrznmpxsxc0jux5f20fwj0z3wqxxk6fpqm7q0va")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
}
fn dummy_unspent_transaction_outputs() -> Vec<(OutPoint, TxOut)> {
let script_pubkey_1 =
Address::from_str("bc1p80lanj0xee8q667aqcnn0xchlykllfsz3gu5skfv9vjsytaujmdqtv52vu")
.unwrap()
.require_network(Network::Bitcoin)
.unwrap()
.script_pubkey();
let out_point_1 = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 0,
};
let utxo_1 = TxOut {
value: DUMMY_UTXO_AMOUNT_INPUT_1,
script_pubkey: script_pubkey_1,
};
let script_pubkey_2 =
Address::from_str("bc1pfd0jmmdnp278vppcw68tkkmquxtq50xchy7f6wdmjtjm7fgsr8dszdcqce")
.unwrap()
.require_network(Network::Bitcoin)
.unwrap()
.script_pubkey();
let out_point_2 = OutPoint {
txid: Txid::all_zeros(), // Obviously invalid.
vout: 1,
};
let utxo_2 = TxOut {
value: DUMMY_UTXO_AMOUNT_INPUT_2,
script_pubkey: script_pubkey_2,
};
vec![(out_point_1, utxo_1), (out_point_2, utxo_2)]
}
fn main() {
let secp = Secp256k1::new();
// Get the individual xprivs we control. In a real application these would come from a stored secret.
let master_xpriv = XPRIV.parse::<Xpriv>().expect("valid xpriv");
let xpriv_input_1 = get_external_address_xpriv(&secp, master_xpriv, 0);
let xpriv_input_2 = get_internal_address_xpriv(&secp, master_xpriv, 0);
let xpriv_change = get_internal_address_xpriv(&secp, master_xpriv, 1);
// Get the PKs
let (pk_input_1, _) = Xpub::from_priv(&secp, &xpriv_input_1)
.public_key
.x_only_public_key();
let (pk_input_2, _) = Xpub::from_priv(&secp, &xpriv_input_2)
.public_key
.x_only_public_key();
let (pk_change, _) = Xpub::from_priv(&secp, &xpriv_change)
.public_key
.x_only_public_key();
// Get the Tap Key Origins
// Map of tap root X-only keys to origin info and leaf hashes contained in it.
let origin_input_1 = get_tap_key_origin(
pk_input_1,
Fingerprint::from_str(MASTER_FINGERPRINT).unwrap(),
"m/86'/0'/0'/0/0".into_derivation_path().expect("valid derivation path"), // First external address.
);
let origin_input_2 = get_tap_key_origin(
pk_input_2,
Fingerprint::from_str(MASTER_FINGERPRINT).unwrap(),
"m/86'/0'/0'/1/0".into_derivation_path().expect("valid derivation path"), // First internal address.
);
let origins = [origin_input_1, origin_input_2];
// Get the unspent outputs that are locked to the key above that we control.
// In a real application these would come from the chain.
let utxos: Vec<TxOut> = dummy_unspent_transaction_outputs()
.into_iter()
.map(|(_, utxo)| utxo)
.collect();
// Get the addresses to send to.
let address = receivers_address();
// The inputs for the transaction we are constructing.
let inputs: Vec<TxIn> = dummy_unspent_transaction_outputs()
.into_iter()
.map(|(outpoint, _)| TxIn {
previous_output: outpoint,
script_sig: ScriptBuf::default(),
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::default(),
})
.collect();
// 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, pk_change, None), // Change comes back to us.
};
// The transaction we want to sign and broadcast.
let unsigned_tx = Transaction {
version: transaction::Version::TWO, // Post BIP 68.
lock_time: absolute::LockTime::ZERO, // Ignore the locktime.
input: inputs, // Input is 0-indexed.
output: vec![spend, change], // Outputs, order does not matter.
};
// Now we'll start the PSBT workflow.
// Step 1: Creator role; that creates,
// and add inputs and outputs to the PSBT.
let mut psbt = Psbt::from_unsigned_tx(unsigned_tx).expect("Could not create PSBT");
// Step 2:Updater role; that adds additional
// information to the PSBT.
let ty = TapSighashType::All.into();
psbt.inputs = vec![
Input {
witness_utxo: Some(utxos[0].clone()),
tap_key_origins: origins[0].clone(),
tap_internal_key: Some(pk_input_1),
sighash_type: Some(ty),
..Default::default()
},
Input {
witness_utxo: Some(utxos[1].clone()),
tap_key_origins: origins[1].clone(),
tap_internal_key: Some(pk_input_2),
sighash_type: Some(ty),
..Default::default()
},
];
// Step 3: Signer role; that signs the PSBT.
psbt.sign(&master_xpriv, &secp).expect("valid signature");
// Step 4: Finalizer role; that finalizes the PSBT.
psbt.inputs.iter_mut().for_each(|input| {
let script_witness = Witness::p2tr_key_spend(&input.tap_key_sig.unwrap());
input.final_script_witness = Some(script_witness);
// Clear all the data fields as per the spec.
input.partial_sigs = BTreeMap::new();
input.sighash_type = None;
input.redeem_script = None;
input.witness_script = None;
input.bip32_derivation = BTreeMap::new();
});
// BOOM! Transaction signed and ready to broadcast.
let signed_tx = psbt.extract_tx().expect("valid transaction");
let serialized_signed_tx = consensus::encode::serialize_hex(&signed_tx);
println!("Transaction Details: {:#?}", signed_tx);
// check with:
// bitcoin-cli decoderawtransaction <RAW_TX> true
println!("Raw Transaction: {}", serialized_signed_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,
Next, we get the individual extended private keys (xpriv) that we control. These are:
- the master xpriv,
- the xprivs for inputs 1 and 2;
these are done with the
get_external_address_xprivandget_internal_address_xprivfunctions. - the xpriv for the change output, also using the
get_internal_address_xprivfunction.
Now, we need the Taproot X-only keys along the origin info and leaf hashes contained in it.
This is done with the get_tap_key_origin function.
The inputs for the transaction we are constructing,
here named utxos,
are created with the dummy_unspent_transaction_outputs function.
let address = receivers_address(); generates a receiver's address address.
All of these are helper functions that we defined earlier.
In let input = TxIn {...} we are instantiating the inputs for the transaction we are constructing
Inside the TxIn struct we are setting the following fields:
previous_outputis the outpoint of the dummy UTXO we are spending; it is aOutPointtype.script_sigis the script code required to spend an output; it is aScriptBuftype. We are instantiating a new empty script withScriptBuf::new().sequenceis the sequence number; it is aSequencetype. We are using theENABLE_RBF_NO_LOCKTIMEconstant.witnessis the witness stack; it is aWitnesstype. We are using thedefaultmethod to create an empty witness that will be filled in later after signing. This is possible becauseWitnessimplements theDefaulttrait.
In let spend = TxOut {...} we are instantiating the spend output.
Inside the TxOut struct we are setting the following fields:
valueis the amount we are spending; it is au64type. We are using theconst SPEND_AMOUNTthat we defined earlier.script_pubkeyis the script code required to spend a P2TR output; it is aScriptBuftype. We are using thescript_pubkeymethod 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 earlier3.
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:
versionis the transaction version; it can be ai32type. However it is best to use theVersionstruct. We are using versionTWOwhich means that BIP 68 applies.lock_timeis the transaction lock time; it is aLockTimeenum. We are using the constantZEROThis will make the transaction valid immediately.inputis the input vector; it is aVec<TxIn>type. We are using theinputvariable that we defined earlier wrapped in thevec!macro for convenient initialization.outputis the output vector; it is aVec<TxOut>type. We are using thespendandchangevariables that we defined earlier wrapped in thevec!macro for convenient initialization.
Now we are ready to start our PSBT workflow.
The first step is the Creator role.
We create a PSBT from the unsigned transaction using the Psbt::from_unsigned_tx method.
Next, we move to the Updater role.
We add additional information to the PSBT.
This is done by setting the psbt.inputs field to a vector of Input structs.
In particular, we set the following fields:
witness_utxois the witness UTXO; it is anOption<TxOut>type. We are using theutxosvector that we defined earlier.tap_key_originsis the Tap Key Origins; it is aBTreeMap<XOnlyPublicKey, (Vec<TapLeafHash>, (Fingerprint, DerivationPath))>type. We are using theoriginsvector that we defined earlier.tap_internal_keyis the Taproot internal key; it is anOption<XOnlyPublicKey>type.sighash_typeis the sighash type; it is anOption<PsbtSighashType>type.
All the other fields are set to their default values using the Default::default() method.
The following step is the Signer role.
Here is were we sign the PSBT with the
sign method.
This method takes the master extended private key and the Secp256k1 context as arguments.
It attempts to create all the required signatures for this PSBT using the extended private key.
Finally, we move to the Finalizer role. Here we finalize the PSBT, making it ready to be extracted into a signed transaction, and if necessary, broadcasted to the Bitcoin network. This is done by setting the following fields:
final_script_witnessis the final script witness; it is anOption<Witness>type. We are using theWitness::p2tr_key_spend()method to create a witness required to do a key path spend of a P2TR output.partial_sigsis the partial signatures; it is aBTreeMap<XOnlyPublicKey, Vec<u8>>type. We are using an empty map.sighash_typeis the sighash type; it is anOption<PsbtSighashType>type. We are using theNonevalue.redeem_scriptis the redeem script; it is anOption<ScriptBuf>type. We are using theNonevalue.witness_scriptis the witness script; it is anOption<ScriptBuf>type.bip32_derivationis the BIP 32 derivation; it is aBTreeMap<Xpub, (Fingerprint, DerivationPath)>type. We are using an empty map.
Finally, we extract the signed transaction from the PSBT using the extract_tx method.
As the last step we print both the transaction details and the raw transaction
to the terminal using the println! macro.
This transaction is now ready to be broadcast to the Bitcoin network.
For anything in production, the step 4 (Finalizer) should be done with the
psbt::PsbtExt from the miniscript crate trait.
It provides a
.finalize_mut
to a Psbt object,
which takes in a mutable reference to Psbt and populates the final_witness and final_scriptsig for all inputs.
-
Please note that the
CHANGE_AMOUNTis not the same as theDUMMY_UTXO_AMOUNT_INPUT_Ns minus theSPEND_AMOUNT. This is due to the fact that we need to pay a miner's fee for the transaction. ↩ -
this is an arbitrary mainnet addresses from block 805222. ↩
-
And also we are locking the output to an address that we control: the
internal_keypublic key hash that we generated earlier. ↩
