Merge rust-bitcoin/rust-bitcoin#2095: bitcoin: Add signing examples

fa104aefa5 bitcoin: Add signing examples (Tobin C. Harding)

Pull request description:

  Add two signing examples to showcase signing a simple one input two output transaction using both segwit v0 outputs and taproot outputs.

  This patch is the result of the recent rust-bitcoin TABConf workshop, with bug fix by Sanket, updated to use APIs from tip of master branch.

  This code, depending on v0.30.0 is what was added to the cookbook.

ACKs for top commit:
  realeinherjar:
    ACK fa104aefa5
  apoelstra:
    ACK fa104aefa5

Tree-SHA512: ce0d5b8291c94387c68b5e1cf740d3267fc00c997af5b96f5be525f348140d9a9af17ab66d556990f09bf081a5a812374cb633ea276100c7c21f218b85eae3fd
This commit is contained in:
Andrew Poelstra 2023-11-22 22:08:16 +00:00
commit 71d92bdbb9
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
4 changed files with 267 additions and 0 deletions

View File

@ -69,6 +69,14 @@ required-features = ["std", "rand-std"]
name = "ecdsa-psbt" name = "ecdsa-psbt"
required-features = ["std", "bitcoinconsensus"] required-features = ["std", "bitcoinconsensus"]
[[example]]
name = "sign-tx-segwit-v0"
required-features = ["std", "rand-std"]
[[example]]
name = "sign-tx-taproot"
required-features = ["std", "rand-std"]
[[example]] [[example]]
name = "taproot-psbt" name = "taproot-psbt"
required-features = ["std", "rand-std", "bitcoinconsensus"] required-features = ["std", "rand-std", "bitcoinconsensus"]

View File

@ -29,6 +29,8 @@ then
cargo clippy --locked --example bip32 -- -D warnings cargo clippy --locked --example bip32 -- -D warnings
cargo clippy --locked --example handshake --features=rand-std -- -D warnings cargo clippy --locked --example handshake --features=rand-std -- -D warnings
cargo clippy --locked --example ecdsa-psbt --features=bitcoinconsensus -- -D warnings cargo clippy --locked --example ecdsa-psbt --features=bitcoinconsensus -- -D warnings
cargo clippy --locked --example sign-tx-segwit-v0 --features=rand-std -- -D warnings
cargo clippy --locked --example sign-tx-taproot --features=rand-std -- -D warnings
cargo clippy --locked --example taproot-psbt --features=rand-std,bitcoinconsensus -- -D warnings cargo clippy --locked --example taproot-psbt --features=rand-std,bitcoinconsensus -- -D warnings
# We should not have any duplicate dependencies. This catches mistakes made upgrading dependencies # We should not have any duplicate dependencies. This catches mistakes made upgrading dependencies
@ -92,6 +94,8 @@ do
done done
cargo run --locked --example ecdsa-psbt --features=bitcoinconsensus cargo run --locked --example ecdsa-psbt --features=bitcoinconsensus
cargo run --locked --example sign-tx-segwit-v0 --features=rand-std -- -D warnings
cargo run --locked --example sign-tx-taproot --features=rand-std -- -D warnings
cargo run --locked --example taproot-psbt --features=rand-std,bitcoinconsensus cargo run --locked --example taproot-psbt --features=rand-std,bitcoinconsensus
# Build the docs if told to (this only works with the nightly toolchain) # Build the docs if told to (this only works with the nightly toolchain)

View File

@ -0,0 +1,125 @@
// SPDX-License-Identifier: CC0-1.0
//! Demonstrate creating a transaction that spends to and from p2wpkh outputs.
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 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, SPEND_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 sig = secp.sign_ecdsa(&msg, &sk);
// Update the witness stack.
let signature = bitcoin::ecdsa::Signature { sig, hash_ty: EcdsaSighashType::All };
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);
}
/// An example of keys controlled by the transaction sender.
///
/// In a real application these would be actual secrets.
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)
}
/// A dummy address for the receiver.
///
/// We lock the spend output to the key associated with this address.
///
/// (FWIW this is a random mainnet address from block 80219.)
fn receivers_address() -> Address {
Address::from_str("bc1q7cyrfmck2ffu2ud3rn5l5a8yv6f0chkp0zpemf")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
}
/// Creates a p2wpkh output locked to the key associated with `wpkh`.
///
/// An utxo is described by the `OutPoint` (txid and index within the transaction that it was
/// created). Using the out point one can get the transaction by `txid` and using the `vout` get the
/// transaction value and script pubkey (`TxOut`) of the utxo.
///
/// This output is locked to keys that we control, in a real application this would be a valid
/// output taken from a transaction that appears in the chain.
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)
}

View File

@ -0,0 +1,130 @@
// SPDX-License-Identifier: CC0-1.0
//! Demonstrate creating a transaction that spends to and from p2tr outputs.
use std::str::FromStr;
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 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 sig = secp.sign_schnorr(&msg, &tweaked.to_inner());
// Update the witness stack.
let signature = bitcoin::taproot::Signature { sig, hash_ty: 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);
}
/// An example of keys controlled by the transaction sender.
///
/// In a real application these would be actual secrets.
fn senders_keys<C: Signing>(secp: &Secp256k1<C>) -> Keypair {
let sk = SecretKey::new(&mut rand::thread_rng());
Keypair::from_secret_key(secp, &sk)
}
/// A dummy address for the receiver.
///
/// We lock the spend output to the key associated with this address.
///
/// (FWIW this is an arbitrary mainnet address from block 805222.)
fn receivers_address() -> Address {
Address::from_str("bc1p0dq0tzg2r780hldthn5mrznmpxsxc0jux5f20fwj0z3wqxxk6fpqm7q0va")
.expect("a valid address")
.require_network(Network::Bitcoin)
.expect("valid address for mainnet")
}
/// Creates a p2wpkh output locked to the key associated with `wpkh`.
///
/// An utxo is described by the `OutPoint` (txid and index within the transaction that it was
/// created). Using the out point one can get the transaction by `txid` and using the `vout` get the
/// transaction value and script pubkey (`TxOut`) of the utxo.
///
/// This output is locked to keys that we control, in a real application this would be a valid
/// output taken from a transaction that appears in the chain.
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)
}