361 lines
13 KiB
Rust
361 lines
13 KiB
Rust
#![cfg(not(feature = "rand-std"))]
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::str::FromStr;
|
|
|
|
use bitcoin::bip32::{DerivationPath, Fingerprint};
|
|
use bitcoin::consensus::encode::serialize_hex;
|
|
use bitcoin::opcodes::all::OP_CHECKSIG;
|
|
use bitcoin::psbt::{GetKey, Input, KeyRequest, PsbtSighashType, SignError};
|
|
use bitcoin::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo};
|
|
use bitcoin::transaction::Version;
|
|
use bitcoin::{
|
|
absolute, script, Address, Network, OutPoint, PrivateKey, Psbt, ScriptBuf, Sequence,
|
|
Transaction, TxIn, TxOut, Witness,
|
|
};
|
|
use secp256k1::{Keypair, Secp256k1, Signing, XOnlyPublicKey};
|
|
use units::Amount;
|
|
|
|
#[test]
|
|
fn psbt_sign_taproot() {
|
|
struct Keystore {
|
|
sk: PrivateKey,
|
|
mfp: Fingerprint,
|
|
}
|
|
|
|
impl GetKey for Keystore {
|
|
type Error = SignError;
|
|
fn get_key<C: Signing>(
|
|
&self,
|
|
key_request: KeyRequest,
|
|
_secp: &Secp256k1<C>,
|
|
) -> Result<Option<PrivateKey>, Self::Error> {
|
|
match key_request {
|
|
KeyRequest::Bip32((mfp, _)) =>
|
|
if mfp == self.mfp {
|
|
Ok(Some(self.sk))
|
|
} else {
|
|
Err(SignError::KeyNotFound)
|
|
},
|
|
_ => Err(SignError::KeyNotFound),
|
|
}
|
|
}
|
|
}
|
|
|
|
let secp = &Secp256k1::new();
|
|
|
|
let sk_path = [
|
|
("dff1c8c2c016a572914b4c5adb8791d62b4768ae9d0a61be8ab94cf5038d7d90", "86'/1'/0'/0/0"),
|
|
("1ede31b0e7e47c2afc65ffd158b1b1b9d3b752bba8fd117dc8b9e944a390e8d9", "86'/1'/0'/0/1"),
|
|
("1fb777f1a6fb9b76724551f8bc8ad91b77f33b8c456d65d746035391d724922a", "86'/1'/0'/0/2"),
|
|
];
|
|
let mfp = "73c5da0a";
|
|
|
|
//
|
|
// Step 0: Create P2TR address.
|
|
//
|
|
|
|
// Create three basic scripts to test script path spend.
|
|
let script1 = create_basic_single_sig_script(secp, sk_path[0].0); // m/86'/1'/0'/0/0
|
|
let script2 = create_basic_single_sig_script(secp, sk_path[1].0); // m/86'/1'/0'/0/1
|
|
let script3 = create_basic_single_sig_script(secp, sk_path[2].0); // m/86'/1'/0'/0/2
|
|
|
|
// Just use one of the secret keys for the key path spend.
|
|
let kp = Keypair::from_seckey_str(secp, &sk_path[2].0).expect("failed to create keypair");
|
|
|
|
let internal_key = kp.x_only_public_key().0; // Ignore the parity.
|
|
|
|
let tree =
|
|
create_taproot_tree(secp, script1.clone(), script2.clone(), script3.clone(), internal_key);
|
|
|
|
let address = create_p2tr_address(tree.clone());
|
|
assert_eq!(
|
|
"tb1pytee2mxz0f4fkrsqqws2lsgnkp8nrw2atjkjy2n9gahggsphr0gszaxxmv",
|
|
address.to_string()
|
|
);
|
|
|
|
// m/86'/1'/0'/0/7
|
|
let to_address = "tb1pyfv094rr0vk28lf8v9yx3veaacdzg26ztqk4ga84zucqqhafnn5q9my9rz";
|
|
let to_address = Address::from_str(to_address).unwrap().assume_checked();
|
|
|
|
// key path spend
|
|
{
|
|
//
|
|
// Step 1: create psbt for key path spend.
|
|
//
|
|
let mut psbt_key_path_spend = create_psbt_for_taproot_key_path_spend(
|
|
address.clone(),
|
|
to_address.clone(),
|
|
tree.clone(),
|
|
);
|
|
|
|
//
|
|
// Step 2: sign psbt.
|
|
//
|
|
let keystore = Keystore {
|
|
mfp: Fingerprint::from_str(mfp).unwrap(),
|
|
sk: PrivateKey::new(kp.secret_key(), Network::Testnet),
|
|
};
|
|
let _ = psbt_key_path_spend.sign(&keystore, secp);
|
|
|
|
let sig = "92864dc9e56b6260ecbd54ec16b94bb597a2e6be7cca0de89d75e17921e0e1528cba32dd04217175c237e1835b5db1c8b384401718514f9443dce933c6ba9c87";
|
|
assert_eq!(sig, psbt_key_path_spend.inputs[0].tap_key_sig.unwrap().signature.to_string());
|
|
|
|
//
|
|
// Step 3: finalize psbt.
|
|
//
|
|
let final_psbt = finalize_psbt_for_key_path_spend(psbt_key_path_spend);
|
|
let tx = final_psbt.extract_tx().unwrap();
|
|
|
|
let tx_id = "5306516f2032d9f34c9f2f6d2b1b8ad2486ef1ba196d8d8d780e59773e48ad6d";
|
|
assert_eq!(tx_id, tx.compute_txid().to_string());
|
|
|
|
let tx_bytes = "020000000001013aee4d6b51da574900e56d173041115bd1e1d01d4697a845784cf716a10c98060000000000ffffffff0100190000000000002251202258f2d4637b2ca3fd27614868b33dee1a242b42582d5474f51730005fa99ce8014092864dc9e56b6260ecbd54ec16b94bb597a2e6be7cca0de89d75e17921e0e1528cba32dd04217175c237e1835b5db1c8b384401718514f9443dce933c6ba9c8700000000";
|
|
let tx_hex = serialize_hex(&tx);
|
|
assert_eq!(tx_bytes, tx_hex);
|
|
}
|
|
|
|
// script path spend
|
|
{
|
|
// use private key of path "m/86'/1'/0'/0/1" as signing key
|
|
let kp = Keypair::from_seckey_str(secp, &sk_path[1].0).expect("failed to create keypair");
|
|
let x_only_pubkey = kp.x_only_public_key().0;
|
|
let signing_key_path = sk_path[1].1;
|
|
|
|
let keystore = Keystore {
|
|
mfp: Fingerprint::from_str(mfp).unwrap(),
|
|
sk: PrivateKey::new(kp.secret_key(), Network::Testnet),
|
|
};
|
|
|
|
//
|
|
// Step 1: create psbt for script path spend.
|
|
//
|
|
let mut psbt_script_path_spend = create_psbt_for_taproot_script_path_spend(
|
|
address.clone(),
|
|
to_address.clone(),
|
|
tree.clone(),
|
|
x_only_pubkey,
|
|
signing_key_path,
|
|
script2.clone(),
|
|
);
|
|
|
|
//
|
|
// Step 2: sign psbt.
|
|
//
|
|
let _ = psbt_script_path_spend.sign(&keystore, secp);
|
|
|
|
let sig = "9c1466e1631a58c55fcb8642ce5f7896314f4b565d92c5c80b17aa9abf56d22e0b5e5dcbcfe836bbd7d409491f58aa9e1f68a491ef8f05eef62fb50ffac85727";
|
|
assert_eq!(
|
|
sig,
|
|
psbt_script_path_spend.inputs[0]
|
|
.tap_script_sigs
|
|
.get(&(x_only_pubkey, script2.clone().tapscript_leaf_hash()))
|
|
.unwrap()
|
|
.signature
|
|
.to_string()
|
|
);
|
|
|
|
//
|
|
// Step 3: finalize psbt.
|
|
//
|
|
let final_psbt = finalize_psbt_for_script_path_spend(psbt_script_path_spend);
|
|
let tx = final_psbt.extract_tx().unwrap();
|
|
|
|
let tx_id = "a51f723beffc810248809355ba9c9e4b39c6e55c08429f0aeaa79b73f18bc2a6";
|
|
assert_eq!(tx_id, tx.compute_txid().to_string());
|
|
|
|
let tx_hex = serialize_hex(&tx);
|
|
let tx_bytes = "0200000000010176a3c94a6b21d742e8ca192130ad10fdfc4c83510cb6baba8572a5fc70677c9d0000000000ffffffff0170170000000000002251202258f2d4637b2ca3fd27614868b33dee1a242b42582d5474f51730005fa99ce803419c1466e1631a58c55fcb8642ce5f7896314f4b565d92c5c80b17aa9abf56d22e0b5e5dcbcfe836bbd7d409491f58aa9e1f68a491ef8f05eef62fb50ffac857270122203058679f6d60b87ef921d98a2a9a1f1e0779dae27bedbd1cdb2f147a07835ac9ac61c1b68df382cad577d8304d5a8e640c3cb42d77c10016ab754caa4d6e68b6cb296d9b9d92a717ebeba858f75182936f0da5a7aecc434b0eebb2dc8a6af5409422ccf87f124e735a592a8ff390a68f6f05469ba8422e246dc78b0b57cd1576ffa98c00000000";
|
|
assert_eq!(tx_bytes, tx_hex);
|
|
}
|
|
}
|
|
|
|
fn create_basic_single_sig_script(secp: &Secp256k1<secp256k1::All>, sk: &str) -> ScriptBuf {
|
|
let kp = Keypair::from_seckey_str(secp, sk).expect("failed to create keypair");
|
|
let x_only_pubkey = kp.x_only_public_key().0;
|
|
script::Builder::new()
|
|
.push_slice(x_only_pubkey.serialize())
|
|
.push_opcode(OP_CHECKSIG)
|
|
.into_script()
|
|
}
|
|
|
|
fn create_taproot_tree(
|
|
secp: &Secp256k1<secp256k1::All>,
|
|
script1: ScriptBuf,
|
|
script2: ScriptBuf,
|
|
script3: ScriptBuf,
|
|
internal_key: XOnlyPublicKey,
|
|
) -> TaprootSpendInfo {
|
|
let builder = TaprootBuilder::new();
|
|
let builder = builder.add_leaf(2, script1).unwrap();
|
|
let builder = builder.add_leaf(2, script2).unwrap();
|
|
let builder = builder.add_leaf(1, script3).unwrap();
|
|
builder.finalize(secp, internal_key).unwrap()
|
|
}
|
|
|
|
fn create_p2tr_address(tree: TaprootSpendInfo) -> Address {
|
|
let output_key = tree.output_key();
|
|
Address::p2tr_tweaked(output_key, Network::Testnet)
|
|
}
|
|
|
|
fn create_psbt_for_taproot_key_path_spend(
|
|
from_address: Address,
|
|
to_address: Address,
|
|
tree: TaprootSpendInfo,
|
|
) -> Psbt {
|
|
let send_value = 6400;
|
|
let out_puts = vec![TxOut {
|
|
value: Amount::from_sat(send_value),
|
|
script_pubkey: to_address.script_pubkey(),
|
|
}];
|
|
let prev_tx_id = "06980ca116f74c7845a897461dd0e1d15b114130176de5004957da516b4dee3a";
|
|
|
|
let transaction = Transaction {
|
|
version: Version(2),
|
|
lock_time: absolute::LockTime::ZERO,
|
|
input: vec![TxIn {
|
|
previous_output: OutPoint { txid: prev_tx_id.parse().unwrap(), vout: 0 },
|
|
script_sig: ScriptBuf::new(),
|
|
sequence: Sequence(0xFFFFFFFF), // Ignore nSequence.
|
|
witness: Witness::default(),
|
|
}],
|
|
output: out_puts,
|
|
};
|
|
|
|
let mut psbt = Psbt::from_unsigned_tx(transaction).unwrap();
|
|
|
|
let mfp = "73c5da0a";
|
|
let internal_key_path = "86'/1'/0'/0/2";
|
|
|
|
let mut origins = BTreeMap::new();
|
|
origins.insert(
|
|
tree.internal_key(),
|
|
(
|
|
vec![],
|
|
(
|
|
Fingerprint::from_str(mfp).unwrap(),
|
|
DerivationPath::from_str(internal_key_path).unwrap(),
|
|
),
|
|
),
|
|
);
|
|
|
|
let utxo_value = 6588;
|
|
let mut input = Input {
|
|
witness_utxo: {
|
|
let script_pubkey = from_address.script_pubkey();
|
|
Some(TxOut { value: Amount::from_sat(utxo_value), script_pubkey })
|
|
},
|
|
tap_key_origins: origins,
|
|
..Default::default()
|
|
};
|
|
let ty = PsbtSighashType::from_str("SIGHASH_DEFAULT").unwrap();
|
|
input.sighash_type = Some(ty);
|
|
input.tap_internal_key = Some(tree.internal_key());
|
|
input.tap_merkle_root = tree.merkle_root();
|
|
psbt.inputs = vec![input];
|
|
psbt
|
|
}
|
|
|
|
fn finalize_psbt_for_key_path_spend(mut psbt: Psbt) -> Psbt {
|
|
psbt.inputs.iter_mut().for_each(|input| {
|
|
let mut script_witness: Witness = Witness::new();
|
|
script_witness.push(input.tap_key_sig.unwrap().to_vec());
|
|
input.final_script_witness = Some(script_witness);
|
|
input.partial_sigs = BTreeMap::new();
|
|
input.sighash_type = None;
|
|
input.redeem_script = None;
|
|
input.witness_script = None;
|
|
input.bip32_derivation = BTreeMap::new();
|
|
});
|
|
psbt
|
|
}
|
|
|
|
fn create_psbt_for_taproot_script_path_spend(
|
|
from_address: Address,
|
|
to_address: Address,
|
|
tree: TaprootSpendInfo,
|
|
x_only_pubkey_of_signing_key: XOnlyPublicKey,
|
|
signing_key_path: &str,
|
|
use_script: ScriptBuf,
|
|
) -> Psbt {
|
|
let utxo_value = 6280;
|
|
let send_value = 6000;
|
|
let mfp = "73c5da0a";
|
|
|
|
let out_puts = vec![TxOut {
|
|
value: Amount::from_sat(send_value),
|
|
script_pubkey: to_address.script_pubkey(),
|
|
}];
|
|
let prev_tx_id = "9d7c6770fca57285babab60c51834cfcfd10ad302119cae842d7216b4ac9a376";
|
|
let transaction = Transaction {
|
|
version: Version(2),
|
|
lock_time: absolute::LockTime::ZERO,
|
|
input: vec![TxIn {
|
|
previous_output: OutPoint { txid: prev_tx_id.parse().unwrap(), vout: 0 },
|
|
script_sig: ScriptBuf::new(),
|
|
sequence: Sequence(0xFFFFFFFF), // Ignore nSequence.
|
|
witness: Witness::default(),
|
|
}],
|
|
output: out_puts,
|
|
};
|
|
|
|
let mut psbt = Psbt::from_unsigned_tx(transaction).unwrap();
|
|
|
|
let mut origins = BTreeMap::new();
|
|
origins.insert(
|
|
x_only_pubkey_of_signing_key,
|
|
(
|
|
vec![use_script.tapscript_leaf_hash()],
|
|
(
|
|
Fingerprint::from_str(mfp).unwrap(),
|
|
DerivationPath::from_str(signing_key_path).unwrap(),
|
|
),
|
|
),
|
|
);
|
|
|
|
let mut tap_scripts = BTreeMap::new();
|
|
tap_scripts.insert(
|
|
tree.control_block(&(use_script.clone(), LeafVersion::TapScript)).unwrap(),
|
|
(use_script.clone(), LeafVersion::TapScript),
|
|
);
|
|
|
|
let mut input = Input {
|
|
witness_utxo: {
|
|
let script_pubkey = from_address.script_pubkey();
|
|
Some(TxOut { value: Amount::from_sat(utxo_value), script_pubkey })
|
|
},
|
|
tap_key_origins: origins,
|
|
tap_scripts,
|
|
..Default::default()
|
|
};
|
|
let ty = PsbtSighashType::from_str("SIGHASH_ALL").unwrap();
|
|
input.sighash_type = Some(ty);
|
|
input.tap_internal_key = Some(tree.internal_key());
|
|
input.tap_merkle_root = tree.merkle_root();
|
|
psbt.inputs = vec![input];
|
|
psbt
|
|
}
|
|
|
|
fn finalize_psbt_for_script_path_spend(mut psbt: Psbt) -> Psbt {
|
|
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.to_vec());
|
|
}
|
|
for (control_block, (script, _)) in input.tap_scripts.iter() {
|
|
script_witness.push(script.to_bytes());
|
|
script_witness.push(control_block.serialize());
|
|
}
|
|
input.final_script_witness = Some(script_witness);
|
|
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;
|
|
});
|
|
psbt
|
|
}
|