examples: Add taproot-psbt workflow example

This example shows how to use the PSBT API for taproot transactions.
We have a simple BIP86-style spend and an example of an inheritance
timelock that can be spent either by the beneficiary via the script
path after a timelock, or via the key path by the benefactor so that
they can refresh the timelock at any time.
This commit is contained in:
Duncan Dean 2022-05-07 22:22:53 +02:00
parent 5e83c602fd
commit 1a89d5230c
No known key found for this signature in database
GPG Key ID: ED357015286A333D
3 changed files with 752 additions and 0 deletions

View File

@ -61,3 +61,7 @@ required-features = ["std"]
[[example]]
name = "ecdsa-psbt"
required-features = ["std", "bitcoinconsensus"]
[[example]]
name = "taproot-psbt"
required-features = ["std", "bitcoinconsensus"]

View File

@ -36,6 +36,7 @@ then
cargo clippy --example bip32 -- -D warnings
cargo clippy --example handshake -- -D warnings
cargo clippy --example ecdsa-psbt --features=bitcoinconsensus -- -D warnings
cargo clippy --example taproot-psbt --features=bitcoinconsensus -- -D warnings
fi
echo "********* Testing std *************"
@ -79,6 +80,7 @@ do
done
cargo run --example ecdsa-psbt --features=bitcoinconsensus
cargo run --example taproot-psbt --features=bitcoinconsensus
# Build the docs if told to (this only works with the nightly toolchain)
if [ "$DO_DOCS" = true ]; then

View File

@ -0,0 +1,746 @@
//! Example of taproot PSBT workflow
// We use the alias `alias bt='bitcoin-cli -regtest'` for brevity.
// Step 0 - Wipe the `regtest` data directory to start from a clean slate.
// Step 1 - Run `bitcoind -regtest -daemon` to start the daemon. Bitcoin Core 23.0+ is required.
// Step 2 -
// 2.1) Run `bt -named createwallet wallet_name=benefactor blank=true` to create a blank wallet with the name "benefactor"
// 2.2) Run `bt -named createwallet wallet_name=beneficiary blank=true` to create a blank wallet with the name "beneficiary"
// 2.3) Create the two aliases:
// alias bt-benefactor='bitcoin-cli -regtest -rpcwallet=benefactor'
// alias bt-beneficiary='bitcoin-cli -regtest -rpcwallet=beneficiary'
//
// 2.4) Import the example descriptors:
// bt-benefactor importdescriptors '[
// { "desc": "tr(tprv8ZgxMBicQKsPd4arFr7sKjSnKFDVMR2JHw9Y8L9nXN4kiok4u28LpHijEudH3mMYoL4pM5UL9Bgdz2M4Cy8EzfErmU9m86ZTw6hCzvFeTg7/86\'/1\'/0\'/1/*)#jzyeered", "active": true, "timestamp": "now", "internal": true },
// { "desc": "tr(tprv8ZgxMBicQKsPd4arFr7sKjSnKFDVMR2JHw9Y8L9nXN4kiok4u28LpHijEudH3mMYoL4pM5UL9Bgdz2M4Cy8EzfErmU9m86ZTw6hCzvFeTg7/86\'/1\'/0\'/0/*)#rkpcykf4", "active": true, "timestamp": "now" }
// ]'
// bt-beneficiary importdescriptors '[
// { "desc": "tr(tprv8ZgxMBicQKsPe72C5c3cugP8b7AzEuNjP4NSC17Dkpqk5kaAmsL6FHwPsVxPpURVqbNwdLAbNqi8Cvdq6nycDwYdKHDjDRYcsMzfshimAUq/86\'/1\'/0\'/1/*)#w4ehwx46", "active": true, "timestamp": "now", "internal": true },
// { "desc": "tr(tprv8ZgxMBicQKsPe72C5c3cugP8b7AzEuNjP4NSC17Dkpqk5kaAmsL6FHwPsVxPpURVqbNwdLAbNqi8Cvdq6nycDwYdKHDjDRYcsMzfshimAUq/86\'/1\'/0\'/0/*)#lpuknn9z", "active": true, "timestamp": "now" }
// ]'
//
// The xpriv and derivation path from the imported descriptors
const BENEFACTOR_XPRIV_STR: &str = "tprv8ZgxMBicQKsPd4arFr7sKjSnKFDVMR2JHw9Y8L9nXN4kiok4u28LpHijEudH3mMYoL4pM5UL9Bgdz2M4Cy8EzfErmU9m86ZTw6hCzvFeTg7";
const BENEFICIARY_XPRIV_STR: &str = "tprv8ZgxMBicQKsPe72C5c3cugP8b7AzEuNjP4NSC17Dkpqk5kaAmsL6FHwPsVxPpURVqbNwdLAbNqi8Cvdq6nycDwYdKHDjDRYcsMzfshimAUq";
const BIP86_DERIVATION_PATH: &str = "m/86'/1'/0'/0/0";
// Step 3 -
// Run `bt generatetoaddress 103 $(bt-benefactor getnewaddress '' bech32m)` to generate 103 new blocks
// with block reward being sent to a newly created P2TR address in the `benefactor` wallet.
// This will leave us with 3 mature UTXOs that can be spent. Each will be used in a different example below.
// Step 4 - Run `bt-benefactor listunspent` to display our three spendable UTXOs. Check that everything is the same as below
// - otherwise modify it. The txids should be deterministic on regtest:
const UTXO_SCRIPT_PUBKEY: &str =
"5120be27fa8b1f5278faf82cab8da23e8761f8f9bd5d5ebebbb37e0e12a70d92dd16";
const UTXO_PUBKEY: &str = "a6ac32163539c16b6b5dbbca01b725b8e8acaa5f821ba42c80e7940062140d19";
const UTXO_MASTER_FINGERPRINT: &str = "e61b318f";
const ABSOLUTE_FEES_IN_SATS: u64 = 1000;
// UTXO_1 will be used for spending example 1
const UTXO_1: P2trUtxo = P2trUtxo {
txid: "a85d89b4666fed622281d3589474aa1f87971b54bd5d9c1899ed2e8e0447cc06",
vout: 0,
script_pubkey: UTXO_SCRIPT_PUBKEY,
pubkey: UTXO_PUBKEY,
master_fingerprint: UTXO_MASTER_FINGERPRINT,
amount_in_sats: 50 * COIN_VALUE, // 50 BTC
derivation_path: BIP86_DERIVATION_PATH,
};
// UTXO_2 will be used for spending example 2
const UTXO_2: P2trUtxo = P2trUtxo {
txid: "6f1c1df5862a67f4b6d1cde9a87e3c441b483ba6a140fbec2815f03aa3a5309d",
vout: 0,
script_pubkey: UTXO_SCRIPT_PUBKEY,
pubkey: UTXO_PUBKEY,
master_fingerprint: UTXO_MASTER_FINGERPRINT,
amount_in_sats: 50 * COIN_VALUE,
derivation_path: BIP86_DERIVATION_PATH,
};
// UTXO_3 will be used for spending example 3
const UTXO_3: P2trUtxo = P2trUtxo {
txid: "9795fed5aedca219244a396dfd7bce55c851274418383c3ab43530e3f74e5dcc",
vout: 0,
script_pubkey: UTXO_SCRIPT_PUBKEY,
pubkey: UTXO_PUBKEY,
master_fingerprint: UTXO_MASTER_FINGERPRINT,
amount_in_sats: 50 * COIN_VALUE,
derivation_path: BIP86_DERIVATION_PATH,
};
use std::collections::BTreeMap;
use std::str::FromStr;
use bitcoin::consensus::encode;
use bitcoin::constants::COIN_VALUE;
use bitcoin::hashes::hex::FromHex;
use bitcoin::hashes::Hash;
use bitcoin::opcodes::all::{OP_CHECKSIG, OP_CLTV, OP_DROP};
use bitcoin::psbt::serialize::Serialize;
use bitcoin::psbt::{self, Input, Output, Psbt, PsbtSighashType};
use bitcoin::schnorr::TapTweak;
use bitcoin::secp256k1::{Message, Secp256k1};
use bitcoin::util::bip32::{
ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint,
};
use bitcoin::util::sighash;
use bitcoin::util::taproot::{
LeafVersion, TapLeafHash, TapSighashHash, TaprootBuilder, TaprootSpendInfo,
};
use bitcoin::{
absolute, script, Address, Amount, OutPoint, SchnorrSig, SchnorrSighashType, Script,
SighashCache, Transaction, TxIn, TxOut, Txid, Witness, XOnlyPublicKey,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let secp = Secp256k1::new();
println!("\n----------------");
println!("\nSTART EXAMPLE 1 - P2TR with a BIP86 commitment, signed with internal key\n");
// Just some addresses for outputs from our wallets. Not really important.
let to_address =
Address::from_str("bcrt1p0p3rvwww0v9znrclp00uneq8ytre9kj922v8fxhnezm3mgsmn9usdxaefc")?;
let change_address =
Address::from_str("bcrt1pz449kexzydh2kaypatup5ultru3ej284t6eguhnkn6wkhswt0l7q3a7j76")?;
let amount_to_send_in_sats = COIN_VALUE;
let change_amount = UTXO_1
.amount_in_sats
.checked_sub(amount_to_send_in_sats)
.and_then(|x| x.checked_sub(ABSOLUTE_FEES_IN_SATS))
.ok_or("Fees more than input amount!")?;
let tx_hex_string = encode::serialize_hex(&generate_bip86_key_spend_tx(
&secp,
// The master extended private key from the descriptor in step 4
ExtendedPrivKey::from_str(BENEFACTOR_XPRIV_STR)?,
// Set these fields with valid data for the UTXO from step 5 above
UTXO_1,
vec![
TxOut { value: amount_to_send_in_sats, script_pubkey: to_address.script_pubkey() },
TxOut { value: change_amount, script_pubkey: change_address.script_pubkey() },
],
)?);
println!(
"\nYou should now be able to broadcast the following transaction: \n\n{}",
tx_hex_string
);
println!("\nEND EXAMPLE 1\n");
println!("----------------\n");
println!("START EXAMPLE 2 - Script path spending of inheritance UTXO\n");
{
let beneficiary =
BeneficiaryWallet::new(ExtendedPrivKey::from_str(BENEFICIARY_XPRIV_STR)?)?;
let mut benefactor = BenefactorWallet::new(
ExtendedPrivKey::from_str(BENEFACTOR_XPRIV_STR)?,
beneficiary.master_xpub(),
)?;
let (tx, psbt) = benefactor.create_inheritance_funding_tx(1000, UTXO_2)?;
let tx_hex = encode::serialize_hex(&tx);
println!("Inheritance funding tx hex:\n\n{}", tx_hex);
// You can now broadcast the transaction hex:
// bt sendrawtransaction ...
//
// And mine a block to confirm the transaction:
// bt generatetoaddress 1 $(bt-benefactor getnewaddress '' 'bech32m')
let spending_tx = beneficiary.spend_inheritance(psbt, 1000, to_address)?;
let spending_tx_hex = encode::serialize_hex(&spending_tx);
println!("\nInheritance spending tx hex:\n\n{}", spending_tx_hex);
// If you try to broadcast now, the transaction will be rejected as it is timelocked.
// First mine 900 blocks so we're sure we are over the 1000 block locktime:
// bt generatetoaddress 900 $(bt-benefactor getnewaddress '' 'bech32m')
// Then broadcast the transaction with `bt sendrawtransaction ...`
}
println!("\nEND EXAMPLE 2\n");
println!("----------------\n");
println!("START EXAMPLE 3 - Key path spending of inheritance UTXO\n");
{
let beneficiary =
BeneficiaryWallet::new(ExtendedPrivKey::from_str(BENEFICIARY_XPRIV_STR)?)?;
let mut benefactor = BenefactorWallet::new(
ExtendedPrivKey::from_str(BENEFACTOR_XPRIV_STR)?,
beneficiary.master_xpub(),
)?;
let (tx, _) = benefactor.create_inheritance_funding_tx(2000, UTXO_3)?;
let tx_hex = encode::serialize_hex(&tx);
println!("Inheritance funding tx hex:\n\n{}", tx_hex);
// You can now broadcast the transaction hex:
// bt sendrawtransaction ...
//
// And mine a block to confirm the transaction:
// bt generatetoaddress 1 $(bt-benefactor getnewaddress '' 'bech32m')
// At some point we may want to extend the locktime further into the future for the beneficiary.
// We can do this by "refreshing" the inheritance transaction as the benefactor. This effectively
// spends the inheritance transaction via the key path of the taproot output, and is not encumbered
// by the timelock so we can spend it immediately. We set up a new output similar to the first with
// a locktime that is 'locktime_delta' blocks greater.
let (tx, _) = benefactor.refresh_tx(1000)?;
let tx_hex = encode::serialize_hex(&tx);
println!("\nRefreshed inheritance tx hex:\n\n{}\n", tx_hex);
println!("\nEND EXAMPLE 3\n");
println!("----------------\n");
}
Ok(())
}
struct P2trUtxo<'a> {
txid: &'a str,
vout: u32,
script_pubkey: &'a str,
pubkey: &'a str,
master_fingerprint: &'a str,
amount_in_sats: u64,
derivation_path: &'a str,
}
fn generate_bip86_key_spend_tx(
secp: &secp256k1::Secp256k1<secp256k1::All>,
master_xpriv: ExtendedPrivKey,
input_utxo: P2trUtxo,
outputs: Vec<TxOut>,
) -> Result<Transaction, Box<dyn std::error::Error>> {
let from_amount = input_utxo.amount_in_sats;
let input_pubkey = XOnlyPublicKey::from_str(input_utxo.pubkey)?;
// CREATOR + UPDATER
let tx1 = Transaction {
version: 2,
lock_time: absolute::PackedLockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
txid: Txid::from_hex(input_utxo.txid)?,
vout: input_utxo.vout,
},
script_sig: Script::new(),
sequence: bitcoin::Sequence(0xFFFFFFFF), // Ignore nSequence.
witness: Witness::default(),
}],
output: outputs,
};
let mut psbt = Psbt::from_unsigned_tx(tx1)?;
let mut origins = BTreeMap::new();
origins.insert(
input_pubkey,
(
vec![],
(
Fingerprint::from_str(input_utxo.master_fingerprint)?,
DerivationPath::from_str(input_utxo.derivation_path)?,
),
),
);
let mut input = Input {
witness_utxo: {
let script_pubkey = Script::from_hex(input_utxo.script_pubkey)
.expect("failed to parse input utxo scriptPubkey");
let amount = Amount::from_sat(from_amount);
Some(TxOut { value: amount.to_sat(), script_pubkey })
},
tap_key_origins: origins,
..Default::default()
};
let ty = PsbtSighashType::from_str("SIGHASH_ALL")?;
input.sighash_type = Some(ty);
input.tap_internal_key = Some(input_pubkey);
psbt.inputs = vec![input];
// SIGNER
let unsigned_tx = psbt.unsigned_tx.clone();
psbt.inputs.iter_mut().enumerate().try_for_each::<_, Result<(), Box<dyn std::error::Error>>>(
|(vout, input)| {
let hash_ty = input
.sighash_type
.and_then(|psbt_sighash_type| psbt_sighash_type.schnorr_hash_ty().ok())
.unwrap_or(bitcoin::SchnorrSighashType::All);
let hash = SighashCache::new(&unsigned_tx).taproot_key_spend_signature_hash(
vout,
&sighash::Prevouts::All(&[TxOut {
value: from_amount,
script_pubkey: Script::from_str(input_utxo.script_pubkey)?,
}]),
hash_ty,
)?;
let (_, (_, derivation_path)) = input
.tap_key_origins
.get(&input.tap_internal_key.ok_or("Internal key missing in PSBT")?)
.ok_or("Missing taproot key origin")?;
let secret_key = master_xpriv.derive_priv(secp, &derivation_path)?.to_priv().inner;
sign_psbt_schnorr(
&secret_key,
input.tap_internal_key.unwrap(),
None,
input,
hash,
hash_ty,
secp,
);
Ok(())
},
)?;
// FINALIZER
psbt.inputs.iter_mut().for_each(|input| {
let mut script_witness: Witness = Witness::new();
script_witness.push(input.tap_key_sig.unwrap().serialize());
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();
});
// EXTRACTOR
let tx = psbt.extract_tx();
tx.verify(|_| {
Some(TxOut {
value: from_amount,
script_pubkey: Script::from_hex(input_utxo.script_pubkey).unwrap(),
})
})
.expect("failed to verify transaction");
Ok(tx)
}
/// A wallet that allows creating and spending from an inheritance directly via the key path for purposes
/// of refreshing the inheritance timelock or changing other spending conditions.
struct BenefactorWallet {
master_xpriv: ExtendedPrivKey,
beneficiary_xpub: ExtendedPubKey,
current_spend_info: Option<TaprootSpendInfo>,
next_psbt: Option<Psbt>,
secp: Secp256k1<secp256k1::All>,
next: ChildNumber,
}
impl BenefactorWallet {
fn new(
master_xpriv: ExtendedPrivKey,
beneficiary_xpub: ExtendedPubKey,
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
master_xpriv,
beneficiary_xpub,
current_spend_info: None,
next_psbt: None,
secp: Secp256k1::new(),
next: ChildNumber::from_normal_idx(0).expect("Zero is a valid child number"),
})
}
fn time_lock_script(locktime: u32, beneficiary_key: XOnlyPublicKey) -> Script {
script::Builder::new()
.push_int(locktime as i64)
.push_opcode(OP_CLTV)
.push_opcode(OP_DROP)
.push_x_only_key(&beneficiary_key)
.push_opcode(OP_CHECKSIG)
.into_script()
}
fn create_inheritance_funding_tx(
&mut self,
lock_time: u32,
input_utxo: P2trUtxo,
) -> Result<(Transaction, Psbt), Box<dyn std::error::Error>> {
if let ChildNumber::Normal { index } = self.next {
if index > 0 && self.current_spend_info.is_some() {
return Err("Transaction already exists, use refresh_inheritance_timelock to refresh the timelock".into());
}
}
// We use some other derivation path in this example for our inheritance protocol. The important thing is to ensure
// that we use an unhardened path so we can make use of xpubs.
let derivation_path = DerivationPath::from_str(&format!("m/101/1/0/0/{}", self.next))?;
let internal_keypair =
self.master_xpriv.derive_priv(&self.secp, &derivation_path)?.to_keypair(&self.secp);
let beneficiary_key =
self.beneficiary_xpub.derive_pub(&self.secp, &derivation_path)?.to_x_only_pub();
// Build up the leaf script and combine with internal key into a taproot commitment
let script = Self::time_lock_script(lock_time, beneficiary_key);
let leaf_hash = TapLeafHash::from_script(&script, LeafVersion::TapScript);
let taproot_spend_info = TaprootBuilder::new()
.add_leaf(0, script.clone())?
.finalize(&self.secp, internal_keypair.x_only_public_key().0)
.expect("Should be finalizable");
self.current_spend_info = Some(taproot_spend_info.clone());
let script_pubkey = Script::new_v1_p2tr(
&self.secp,
taproot_spend_info.internal_key(),
taproot_spend_info.merkle_root(),
);
let value = input_utxo.amount_in_sats - ABSOLUTE_FEES_IN_SATS;
// Spend a normal BIP86-like output as an input in our inheritance funding transaction
let tx = generate_bip86_key_spend_tx(
&self.secp,
self.master_xpriv,
input_utxo,
vec![TxOut { script_pubkey: script_pubkey.clone(), value }],
)?;
// CREATOR + UPDATER
let next_tx = Transaction {
version: 2,
lock_time: absolute::PackedLockTime(lock_time),
input: vec![TxIn {
previous_output: OutPoint { txid: tx.txid(), vout: 0 },
script_sig: Script::new(),
sequence: bitcoin::Sequence(0xFFFFFFFD), // enable locktime and opt-in RBF
witness: Witness::default(),
}],
output: vec![],
};
let mut next_psbt = Psbt::from_unsigned_tx(next_tx)?;
let mut origins = BTreeMap::new();
origins.insert(
beneficiary_key,
(vec![leaf_hash], (self.beneficiary_xpub.fingerprint(), derivation_path.clone())),
);
origins.insert(
internal_keypair.x_only_public_key().0,
(vec![], (self.master_xpriv.fingerprint(&self.secp), derivation_path)),
);
let ty = PsbtSighashType::from_str("SIGHASH_ALL")?;
let mut tap_scripts = BTreeMap::new();
tap_scripts.insert(
taproot_spend_info.control_block(&(script.clone(), LeafVersion::TapScript)).unwrap(),
(script, LeafVersion::TapScript),
);
let input = Input {
witness_utxo: {
let script_pubkey = script_pubkey;
let amount = Amount::from_sat(value);
Some(TxOut { value: amount.to_sat(), script_pubkey })
},
tap_key_origins: origins,
tap_merkle_root: taproot_spend_info.merkle_root(),
sighash_type: Some(ty),
tap_internal_key: Some(internal_keypair.x_only_public_key().0),
tap_scripts,
..Default::default()
};
next_psbt.inputs = vec![input];
self.next_psbt = Some(next_psbt.clone());
self.next.increment()?;
Ok((tx, next_psbt))
}
fn refresh_tx(
&mut self,
lock_time_delta: u32,
) -> Result<(Transaction, Psbt), Box<dyn std::error::Error>> {
if let Some(ref spend_info) = self.current_spend_info.clone() {
let mut psbt = self.next_psbt.clone().expect("Should have next_psbt");
let input = &mut psbt.inputs[0];
let input_value = input.witness_utxo.as_ref().unwrap().value;
let output_value = input_value - ABSOLUTE_FEES_IN_SATS;
// We use some other derivation path in this example for our inheritance protocol. The important thing is to ensure
// that we use an unhardened path so we can make use of xpubs.
let new_derivation_path =
DerivationPath::from_str(&format!("m/101/1/0/0/{}", self.next))?;
let new_internal_keypair = self
.master_xpriv
.derive_priv(&self.secp, &new_derivation_path)?
.to_keypair(&self.secp);
let beneficiary_key =
self.beneficiary_xpub.derive_pub(&self.secp, &new_derivation_path)?.to_x_only_pub();
// Build up the leaf script and combine with internal key into a taproot commitment
let lock_time = psbt.unsigned_tx.lock_time.to_u32() + lock_time_delta;
let script = Self::time_lock_script(lock_time, beneficiary_key);
let leaf_hash = TapLeafHash::from_script(&script, LeafVersion::TapScript);
let taproot_spend_info = TaprootBuilder::new()
.add_leaf(0, script.clone())?
.finalize(&self.secp, new_internal_keypair.x_only_public_key().0)
.expect("Should be finalizable");
self.current_spend_info = Some(taproot_spend_info.clone());
let prevout_script_pubkey = input.witness_utxo.as_ref().unwrap().script_pubkey.clone();
let output_script_pubkey = Script::new_v1_p2tr(
&self.secp,
taproot_spend_info.internal_key(),
taproot_spend_info.merkle_root(),
);
psbt.unsigned_tx.output =
vec![TxOut { script_pubkey: output_script_pubkey.clone(), value: output_value }];
psbt.outputs = vec![Output::default()];
psbt.unsigned_tx.lock_time = absolute::PackedLockTime::ZERO;
let hash_ty = input
.sighash_type
.and_then(|psbt_sighash_type| psbt_sighash_type.schnorr_hash_ty().ok())
.unwrap_or(SchnorrSighashType::All);
let hash = SighashCache::new(&psbt.unsigned_tx).taproot_key_spend_signature_hash(
0,
&sighash::Prevouts::All(&[TxOut {
value: input_value,
script_pubkey: prevout_script_pubkey,
}]),
hash_ty,
)?;
{
let (_, (_, derivation_path)) = input
.tap_key_origins
.get(&input.tap_internal_key.ok_or("Internal key missing in PSBT")?)
.ok_or("Missing taproot key origin")?;
let secret_key =
self.master_xpriv.derive_priv(&self.secp, &derivation_path)?.to_priv().inner;
sign_psbt_schnorr(
&secret_key,
spend_info.internal_key(),
None,
input,
hash,
hash_ty,
&self.secp,
);
}
// FINALIZER
psbt.inputs.iter_mut().for_each(|input| {
let mut script_witness: Witness = Witness::new();
script_witness.push(input.tap_key_sig.unwrap().serialize());
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();
});
// EXTRACTOR
let tx = psbt.extract_tx();
tx.verify(|_| {
Some(TxOut { value: input_value, script_pubkey: output_script_pubkey.clone() })
})
.expect("failed to verify transaction");
let next_tx = Transaction {
version: 2,
lock_time: absolute::PackedLockTime(lock_time),
input: vec![TxIn {
previous_output: OutPoint { txid: tx.txid(), vout: 0 },
script_sig: Script::new(),
sequence: bitcoin::Sequence(0xFFFFFFFD), // enable locktime and opt-in RBF
witness: Witness::default(),
}],
output: vec![],
};
let mut next_psbt = Psbt::from_unsigned_tx(next_tx)?;
let mut origins = BTreeMap::new();
origins.insert(
beneficiary_key,
(vec![leaf_hash], (self.beneficiary_xpub.fingerprint(), new_derivation_path)),
);
let ty = PsbtSighashType::from_str("SIGHASH_ALL")?;
let mut tap_scripts = BTreeMap::new();
tap_scripts.insert(
taproot_spend_info
.control_block(&(script.clone(), LeafVersion::TapScript))
.unwrap(),
(script, LeafVersion::TapScript),
);
let input = Input {
witness_utxo: {
let script_pubkey = output_script_pubkey;
let amount = Amount::from_sat(output_value);
Some(TxOut { value: amount.to_sat(), script_pubkey })
},
tap_key_origins: origins,
tap_merkle_root: taproot_spend_info.merkle_root(),
sighash_type: Some(ty),
tap_internal_key: Some(new_internal_keypair.x_only_public_key().0),
tap_scripts,
..Default::default()
};
next_psbt.inputs = vec![input];
self.next_psbt = Some(next_psbt.clone());
self.next.increment()?;
Ok((tx, next_psbt))
} else {
Err("No current_spend_info available. Create an inheritance tx first.".into())
}
}
}
/// A wallet that allows spending from an inheritance locked to a P2TR UTXO via a script path
/// after some expiry using CLTV.
struct BeneficiaryWallet {
master_xpriv: ExtendedPrivKey,
secp: secp256k1::Secp256k1<secp256k1::All>,
}
impl BeneficiaryWallet {
fn new(master_xpriv: ExtendedPrivKey) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self { master_xpriv, secp: Secp256k1::new() })
}
fn master_xpub(&self) -> ExtendedPubKey {
ExtendedPubKey::from_priv(&self.secp, &self.master_xpriv)
}
fn spend_inheritance(
&self,
mut psbt: Psbt,
lock_time: u32,
to_address: Address,
) -> Result<Transaction, Box<dyn std::error::Error>> {
let input_value = psbt.inputs[0].witness_utxo.as_ref().unwrap().value;
let input_script_pubkey =
psbt.inputs[0].witness_utxo.as_ref().unwrap().script_pubkey.clone();
psbt.unsigned_tx.lock_time = absolute::PackedLockTime(lock_time);
psbt.unsigned_tx.output = vec![TxOut {
script_pubkey: to_address.script_pubkey(),
value: input_value - ABSOLUTE_FEES_IN_SATS,
}];
psbt.outputs = vec![Output::default()];
let unsigned_tx = psbt.unsigned_tx.clone();
// SIGNER
for (x_only_pubkey, (leaf_hashes, (_, derivation_path))) in
&psbt.inputs[0].tap_key_origins.clone()
{
let secret_key =
self.master_xpriv.derive_priv(&self.secp, &derivation_path)?.to_priv().inner;
for lh in leaf_hashes {
let hash_ty = bitcoin::SchnorrSighashType::All;
let hash = SighashCache::new(&unsigned_tx).taproot_script_spend_signature_hash(
0,
&sighash::Prevouts::All(&[TxOut {
value: input_value,
script_pubkey: input_script_pubkey.clone(),
}]),
*lh,
hash_ty,
)?;
sign_psbt_schnorr(
&secret_key,
*x_only_pubkey,
Some(*lh),
&mut psbt.inputs[0],
hash,
hash_ty,
&self.secp,
);
}
}
// FINALIZER
psbt.inputs.iter_mut().for_each(|input| {
let mut script_witness: Witness = Witness::new();
for (_, signature) in input.tap_script_sigs.iter() {
script_witness.push(signature.serialize());
}
for (control_block, (script, _)) in input.tap_scripts.iter() {
script_witness.push(script.serialize());
script_witness.push(control_block.serialize());
}
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();
input.tap_script_sigs = BTreeMap::new();
input.tap_scripts = BTreeMap::new();
input.tap_key_sig = None;
});
// EXTRACTOR
let tx = psbt.extract_tx();
tx.verify(|_| {
Some(TxOut { value: input_value, script_pubkey: input_script_pubkey.clone() })
})
.expect("failed to verify transaction");
Ok(tx)
}
}
// Lifted and modified from BDK at https://github.com/bitcoindevkit/bdk/blob/8fbe40a9181cc9e22cabfc04d57dac5d459da87d/src/wallet/signer.rs#L469-L503
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
// Calling this with `leaf_hash` = `None` will sign for key-spend
fn sign_psbt_schnorr(
secret_key: &secp256k1::SecretKey,
pubkey: XOnlyPublicKey,
leaf_hash: Option<TapLeafHash>,
psbt_input: &mut psbt::Input,
hash: TapSighashHash,
hash_ty: SchnorrSighashType,
secp: &Secp256k1<secp256k1::All>,
) {
let keypair = secp256k1::KeyPair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
let keypair = match leaf_hash {
None => keypair.tap_tweak(secp, psbt_input.tap_merkle_root).to_inner(),
Some(_) => keypair, // no tweak for script spend
};
let sig = secp.sign_schnorr(&Message::from_slice(&hash.into_inner()[..]).unwrap(), &keypair);
let final_signature = SchnorrSig { sig, hash_ty };
if let Some(lh) = leaf_hash {
psbt_input.tap_script_sigs.insert((pubkey, lh), final_signature);
} else {
psbt_input.tap_key_sig = Some(final_signature);
}
}