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: ↩