Merge #985: Taproot support
4bb2372a63bb4b36ca9f0dd91d2373cff3e9e9a4 qa: drop specific value assertions in coin selection test (Antoine Poinsot) 7eb024afbc43ac73aa29d6b892bc4833067c5a3f fuzz: alternate between generating wsh() and tr() descs (Antoine Poinsot) b9a462519791b64165719891c6f0e9c867a07372 ci: run functional tests both under Taproot and P2WSH (Antoine Poinsot) 687a0c2816290d09bda8f1323232bc4c643e9a35 qa: adapt hardcoded coin selection tests to Taproot (Antoine Poinsot) ecef6bff5eaf00305941c04d93a8bac8003f9d34 qa: functional tests lianad using Taproot descriptors (Antoine Poinsot) 96d30db5b868c8f2e6ecc45f1bf6759df582569e signer: taproot support in hot signer (Antoine Poinsot) 80a7dc32aad43fe4c6a1cd0fc41d4ac1bb8a7de6 signer: move p2wsh signing into a dedicated function (Antoine Poinsot) d6222583be148a6b12caed7b0ede00be317d89c6 command: also update Taproot sigs in 'updatespend' (Antoine Poinsot) 714bd3c4eda1420142b5761623808f73c64578c6 spend: check for either p2wsh or Taproot sigs in sanity checks (Antoine Poinsot) e05039f67b3d7a44cd9079f3a57d75a3161cb3f1 spend: don't populate non_witness_utxo for Taproot (Antoine Poinsot) 8596ca76f3547c77cf64c556966ee1651550f231 bitcoind: compare descriptors, not their string representation. (Antoine Poinsot) 602c862118ff43616b24b5d252321a98a83d2454 bitcoind: sanity check min supported version for Taproot descriptor (Antoine Poinsot) d3b7e4cc44c59b8aad77b0cf0d9ae29095db65f9 config: unit test a valid config with a Taproot descriptor (Antoine Poinsot) 6cf8eaab3b92a7f5bcc9c35490dc9766d071ca00 config: deser_from_str isn't descriptor specific. (Antoine Poinsot) 04f4b8a744cdcc52e345948ed9636640d641c528 descriptors: Taproot support (Antoine Poinsot) c897d41965cf077fda63cc39c31cb75f400d757e descriptors: encapsulate key matching logic (Antoine Poinsot) 85fdc40366dcb3c5c8cf91be1b1cc59727402e04 descriptor: encapsulate PSBT in/out information update (Antoine Poinsot) Pull request description: This introduces Taproot support in the Liana daemon / core library. We start by introducing support for `tr()` descriptors alongside `wsh()` descriptors in the Taproot modules. For Taproot-Liana descriptors whose primary spending path isn't a single key, we deterministically derive an unspendable internal key as per https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304/21. This is to allow signing devices to not display the internal key as a spendable key when verifying a descriptor. Currently signing device vendors signaled willingness to implement this scheme. We then adapt the PSBT management logic to use Taproot fields when necessary and upgrade the hot signer to provide Schnorr signatures depending on the PSBT information. Finally, the functional tests are adapted to be able to run the whole test suite under either P2WSH or Taproot. This is done in a somewhat hacky way as i bailed out of re-implementing a Taproot PSBT signer and finalizer in Python after implementing [TapMiniscript support in upstream python-bip380](https://github.com/darosior/python-bip380/pull/23). Instead we use a small Rust program to sign PSBTs for now and we skip a test which explicitly requires an external finalizer. ACKs for top commit: darosior: ACK 4bb2372a63bb4b36ca9f0dd91d2373cff3e9e9a4 -- this underwent multiple rounds of review, Edouard tested it in his follow-up PR to the GUI and i wrote a couple fuzz targets exercising part of the logic introduced here Tree-SHA512: 426032f0bcf8a27e43cf3d158da011bda648cb56534ea678d3c4a43c3f59047e4bb7f83469fd38f70ac962d1a9721e9c1f68baa60bf597bf08fa3c39fc00ebe8
This commit is contained in:
commit
b22f22f0bc
14
.cirrus.yml
14
.cirrus.yml
@ -16,12 +16,19 @@ task:
|
||||
matrix:
|
||||
- USE_MIN_BITCOIN_VERSION: 'TRUE'
|
||||
- USE_MIN_BITCOIN_VERSION: 'FALSE'
|
||||
- USE_TAPROOT: 1
|
||||
- name: 'RPC functional tests'
|
||||
env:
|
||||
TEST_GROUP: tests/test_rpc.py
|
||||
matrix:
|
||||
- USE_TAPROOT: 0
|
||||
- USE_TAPROOT: 1
|
||||
- name: 'Chain functional tests'
|
||||
env:
|
||||
TEST_GROUP: tests/test_chain.py
|
||||
matrix:
|
||||
- USE_TAPROOT: 0
|
||||
- USE_TAPROOT: 1
|
||||
|
||||
cargo_registry_cache:
|
||||
folders: $CARGO_HOME/registry
|
||||
@ -34,7 +41,12 @@ task:
|
||||
fingerprint_script:
|
||||
- rustc --version
|
||||
- cat Cargo.lock
|
||||
lianad_build_script: cargo build --release
|
||||
tests_tools_cache:
|
||||
folder: tests/tools/taproot_signer/target
|
||||
fingerprint_script:
|
||||
- rustc --version
|
||||
- cat tests/tools/taproot_signer/Cargo.lock
|
||||
lianad_build_script: cargo build --release && cd tests/tools/taproot_signer && cargo build --release
|
||||
|
||||
deps_script: apt update && apt install -y python3 python3-pip
|
||||
|
||||
|
||||
@ -67,10 +67,12 @@ fuzz_target!(|data: &[u8]| {
|
||||
desc.receive_descriptor().derive(der_index, &SECP256K1),
|
||||
desc.change_descriptor().derive(der_index, &SECP256K1),
|
||||
];
|
||||
let mut psbt_in = Default::default();
|
||||
let mut psbt_out = Default::default();
|
||||
for desc in der_descs {
|
||||
desc.address(Network::Bitcoin);
|
||||
desc.script_pubkey();
|
||||
desc.witness_script();
|
||||
desc.bip32_derivations();
|
||||
desc.update_psbt_in(&mut psbt_in);
|
||||
desc.update_change_psbt_out(&mut psbt_out);
|
||||
}
|
||||
});
|
||||
|
||||
@ -101,6 +101,7 @@ struct Config {
|
||||
pub recovery_paths: Vec<RecPathConfig>,
|
||||
pub der_index: u16,
|
||||
pub dummy_psbt: DummyPsbt,
|
||||
pub use_taproot: bool,
|
||||
}
|
||||
|
||||
fuzz_target!(|config: Config| {
|
||||
@ -126,7 +127,12 @@ fuzz_target!(|config: Config| {
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let policy = if let Ok(policy) = LianaPolicy::new(prim_path_info, rec_paths_info) {
|
||||
let policy = if config.use_taproot {
|
||||
LianaPolicy::new(prim_path_info, rec_paths_info)
|
||||
} else {
|
||||
LianaPolicy::new_legacy(prim_path_info, rec_paths_info)
|
||||
};
|
||||
let policy = if let Ok(policy) = policy {
|
||||
policy
|
||||
} else {
|
||||
return;
|
||||
@ -154,11 +160,13 @@ fuzz_target!(|config: Config| {
|
||||
desc.receive_descriptor().derive(der_index, &SECP256K1),
|
||||
desc.change_descriptor().derive(der_index, &SECP256K1),
|
||||
];
|
||||
let mut psbt_in = Default::default();
|
||||
let mut psbt_out = Default::default();
|
||||
for desc in &der_descs {
|
||||
desc.address(Network::Bitcoin);
|
||||
desc.script_pubkey();
|
||||
desc.witness_script();
|
||||
desc.bip32_derivations();
|
||||
desc.update_psbt_in(&mut psbt_in);
|
||||
desc.update_change_psbt_out(&mut psbt_out);
|
||||
}
|
||||
|
||||
// Exercise the methods gathering information from a PSBT. TODO: get more useful PSBTs.
|
||||
@ -172,12 +180,11 @@ fuzz_target!(|config: Config| {
|
||||
// for outputs.
|
||||
let rec_desc = &der_descs[0];
|
||||
for psbt_in in psbt.inputs.iter_mut() {
|
||||
psbt_in.witness_script = Some(rec_desc.witness_script());
|
||||
psbt_in.bip32_derivation = rec_desc.bip32_derivations();
|
||||
rec_desc.update_psbt_in(psbt_in);
|
||||
}
|
||||
let change_desc = &der_descs[1];
|
||||
for psbt_out in psbt.outputs.iter_mut() {
|
||||
psbt_out.bip32_derivation = change_desc.bip32_derivations();
|
||||
change_desc.update_change_psbt_out(psbt_out);
|
||||
}
|
||||
|
||||
// Now get the spend info again with these info.
|
||||
|
||||
@ -45,6 +45,9 @@ const BITCOIND_RETRY_LIMIT: usize = 60;
|
||||
// The minimum bitcoind version that can be used with lianad.
|
||||
const MIN_BITCOIND_VERSION: u64 = 240000;
|
||||
|
||||
// The minimum bitcoind version that can be used with lianad and a Taproot descriptor.
|
||||
const MIN_TAPROOT_BITCOIND_VERSION: u64 = 260000;
|
||||
|
||||
/// An error in the bitcoind interface.
|
||||
#[derive(Debug)]
|
||||
pub enum BitcoindError {
|
||||
@ -129,8 +132,8 @@ impl std::fmt::Display for BitcoindError {
|
||||
BitcoindError::InvalidVersion(v) => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid bitcoind version '{}', minimum supported is '{}'.",
|
||||
v, MIN_BITCOIND_VERSION
|
||||
"Invalid bitcoind version '{}', minimum supported is '{}' and minimum supported if using Taproot is '{}'.",
|
||||
v, MIN_BITCOIND_VERSION, MIN_TAPROOT_BITCOIND_VERSION
|
||||
)
|
||||
}
|
||||
BitcoindError::NetworkMismatch(conf_net, bitcoind_net) => {
|
||||
@ -699,12 +702,16 @@ impl BitcoinD {
|
||||
pub fn node_sanity_checks(
|
||||
&self,
|
||||
config_network: bitcoin::Network,
|
||||
is_taproot: bool,
|
||||
) -> Result<(), BitcoindError> {
|
||||
// Check the minimum supported bitcoind version
|
||||
let version = self.get_bitcoind_version();
|
||||
if version < MIN_BITCOIND_VERSION {
|
||||
return Err(BitcoindError::InvalidVersion(version));
|
||||
}
|
||||
if is_taproot && version < MIN_TAPROOT_BITCOIND_VERSION {
|
||||
return Err(BitcoindError::InvalidVersion(version));
|
||||
}
|
||||
|
||||
// Check bitcoind is running on the right network
|
||||
let bitcoind_net = self.get_network_bip70();
|
||||
@ -747,13 +754,27 @@ impl BitcoinD {
|
||||
// Check our main descriptor is imported in this wallet.
|
||||
let receive_desc = main_descriptor.receive_descriptor();
|
||||
let change_desc = main_descriptor.change_descriptor();
|
||||
let desc_list: Vec<String> = self
|
||||
let desc_list: Vec<_> = self
|
||||
.list_descriptors()
|
||||
.into_iter()
|
||||
.map(|entry| entry.desc)
|
||||
.filter_map(|entry| {
|
||||
match descriptor::Descriptor::<descriptor::DescriptorPublicKey>::from_str(
|
||||
&entry.desc,
|
||||
) {
|
||||
Ok(desc) => Some(desc),
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Error deserializing descriptor: {}. Descriptor: {}.",
|
||||
e,
|
||||
entry.desc
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !desc_list.contains(&receive_desc.to_string())
|
||||
|| !desc_list.contains(&change_desc.to_string())
|
||||
if !desc_list.iter().any(|desc| *receive_desc == *desc)
|
||||
|| !desc_list.iter().any(|desc| *change_desc == *desc)
|
||||
{
|
||||
return Err(BitcoindError::Wallet(
|
||||
self.watchonly_wallet_path.clone(),
|
||||
|
||||
@ -591,6 +591,12 @@ impl DaemonControl {
|
||||
psbtin
|
||||
.partial_sigs
|
||||
.extend(db_psbtin.partial_sigs.clone().into_iter());
|
||||
psbtin
|
||||
.tap_script_sigs
|
||||
.extend(db_psbtin.tap_script_sigs.clone().into_iter());
|
||||
if psbtin.tap_key_sig.is_none() {
|
||||
psbtin.tap_key_sig = db_psbtin.tap_key_sig;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the transaction doesn't exist in DB already, sanity check its inputs.
|
||||
|
||||
@ -14,7 +14,7 @@ where
|
||||
{
|
||||
let string = String::deserialize(deserializer)?;
|
||||
T::from_str(&string)
|
||||
.map_err(|e| de::Error::custom(format!("Error parsing descriptor '{}': '{}'", string, e)))
|
||||
.map_err(|e| de::Error::custom(format!("Error parsing '{}': {}", string, e)))
|
||||
}
|
||||
|
||||
pub fn serialize_to_string<T: std::fmt::Display, S: Serializer>(
|
||||
@ -319,6 +319,26 @@ mod tests {
|
||||
#[cfg(unix)] // On non-UNIX there is no 'daemon' member.
|
||||
assert_eq!(toml_str, serialized);
|
||||
|
||||
// A valid, round-tripping, config for a Taproot descriptor.
|
||||
let toml_str = r#"
|
||||
data_dir = '/home/wizardsardine/custom/folder/'
|
||||
daemon = false
|
||||
log_level = 'TRACE'
|
||||
main_descriptor = 'tr([abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,and_v(v:pk([abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560)))#0mt7e93c'
|
||||
|
||||
[bitcoin_config]
|
||||
network = 'bitcoin'
|
||||
poll_interval_secs = 18
|
||||
|
||||
[bitcoind_config]
|
||||
cookie_path = '/home/user/.bitcoin/.cookie'
|
||||
addr = '127.0.0.1:8332'
|
||||
"#.trim_start().replace(" ", "");
|
||||
let parsed = toml::from_str::<Config>(&toml_str).expect("Deserializing toml_str");
|
||||
let serialized = toml::to_string_pretty(&parsed).expect("Serializing to toml");
|
||||
#[cfg(unix)] // On non-UNIX there is no 'daemon' member.
|
||||
assert_eq!(toml_str, serialized);
|
||||
|
||||
// A valid, round-tripping, config with `auth` instead of `cookie_path`
|
||||
let toml_str = r#"
|
||||
data_dir = '/home/wizardsardine/custom/folder/'
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
use miniscript::{
|
||||
bitcoin::{bip32, Sequence},
|
||||
bitcoin::{
|
||||
self, bip32,
|
||||
hashes::{sha256, Hash},
|
||||
secp256k1, Sequence,
|
||||
},
|
||||
descriptor,
|
||||
policy::{compiler, Concrete as ConcretePolicy, Liftable, Semantic as SemanticPolicy},
|
||||
policy::{Concrete as ConcretePolicy, Liftable, Semantic as SemanticPolicy},
|
||||
ScriptContext,
|
||||
};
|
||||
|
||||
@ -9,6 +13,7 @@ use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
convert::TryFrom,
|
||||
error, fmt,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -22,10 +27,10 @@ pub enum LianaPolicyError {
|
||||
InvalidMultiThresh(usize),
|
||||
InvalidMultiKeys(usize),
|
||||
IncompatibleDesc,
|
||||
PolicyAnalysis(miniscript::Error),
|
||||
/// The spending policy is not a valid Miniscript policy: it may for instance be malleable, or
|
||||
/// overflow some limit.
|
||||
InvalidPolicy(compiler::CompilerError),
|
||||
PolicyAnalysis(miniscript::Error),
|
||||
InvalidPolicy(miniscript::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LianaPolicyError {
|
||||
@ -357,6 +362,91 @@ impl PathInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// See
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs:
|
||||
// > One example of such a point is H =
|
||||
// > lift_x(0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0) which is constructed
|
||||
// > by taking the hash of the standard uncompressed encoding of the secp256k1 base point G as X
|
||||
// > coordinate.
|
||||
fn bip341_nums() -> secp256k1::PublicKey {
|
||||
secp256k1::PublicKey::from_str(
|
||||
"0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
|
||||
)
|
||||
.expect("Valid pubkey: NUMS from BIP341")
|
||||
}
|
||||
|
||||
// Given a descpubkey, extract its xpub assuming it is a multixpub. Returns None otherwise.
|
||||
fn get_multi_xkey(desc_key: &descriptor::DescriptorPublicKey) -> Option<&bip32::Xpub> {
|
||||
if let descriptor::DescriptorPublicKey::MultiXPub(descriptor::DescriptorMultiXKey {
|
||||
xkey,
|
||||
..
|
||||
}) = desc_key
|
||||
{
|
||||
Some(xkey)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Construct an unspendable xpub to be used as internal key in a Taproot descriptor, in a way which
|
||||
// could eventually be standardized into wallet policies for a signer to display to the user
|
||||
// "UNSPENDABLE" upon registration (instead of a meaningless key).
|
||||
// See https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304/21.
|
||||
//
|
||||
// Returns `None` if:
|
||||
// - The given descriptor does not contain a Taptree with at least a key in each leaf.
|
||||
// - The keys contained in the descriptor aren't all MultiXPub's.
|
||||
fn unspendable_internal_xpub(
|
||||
desc: &descriptor::Tr<descriptor::DescriptorPublicKey>,
|
||||
) -> Option<bip32::Xpub> {
|
||||
let tap_tree = desc.tap_tree().as_ref()?;
|
||||
|
||||
// Fetch the network to use for the unspendable key from the first key in the descriptor.
|
||||
let first_key = tap_tree.iter().flat_map(|(_, ms)| ms.iter_pk()).next()?;
|
||||
let network = get_multi_xkey(&first_key)?.network;
|
||||
|
||||
// Compute the chaincode to use for the xpub. This is the sha256() of the concatenation of all
|
||||
// the xpubs' pubkey part in the Taptree.
|
||||
let concat =
|
||||
tap_tree
|
||||
.iter()
|
||||
.flat_map(|(_, ms)| ms.iter_pk())
|
||||
.try_fold(Vec::new(), |mut acc, pk| {
|
||||
let xkey = get_multi_xkey(&pk)?;
|
||||
acc.extend_from_slice(&xkey.public_key.serialize());
|
||||
Some(acc)
|
||||
})?;
|
||||
let chain_code = bip32::ChainCode::from(sha256::Hash::hash(&concat).as_ref());
|
||||
|
||||
// Construct the unspendable key. The pubkey part is always BIP341's NUMS.
|
||||
let public_key = bip341_nums();
|
||||
Some(bip32::Xpub {
|
||||
public_key,
|
||||
chain_code,
|
||||
depth: 0,
|
||||
parent_fingerprint: [0; 4].into(),
|
||||
child_number: 0.into(),
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
fn unspendable_internal_key(
|
||||
desc: &descriptor::Tr<descriptor::DescriptorPublicKey>,
|
||||
) -> Option<descriptor::DescriptorPublicKey> {
|
||||
Some(descriptor::DescriptorPublicKey::MultiXPub(
|
||||
descriptor::DescriptorMultiXKey {
|
||||
origin: None,
|
||||
xkey: unspendable_internal_xpub(desc)?,
|
||||
derivation_paths: descriptor::DerivPaths::new(vec![
|
||||
[0.into()][..].into(),
|
||||
[1.into()][..].into(),
|
||||
])
|
||||
.expect("Non empty vec"),
|
||||
wildcard: descriptor::Wildcard::Unhardened,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// A Liana spending policy is one composed of at least two spending paths:
|
||||
/// - A directly available path with any number of keys checks; or
|
||||
/// - One or more recovery paths with any number of keys checks, behind increasing relative
|
||||
@ -370,13 +460,15 @@ impl PathInfo {
|
||||
pub struct LianaPolicy {
|
||||
pub(super) primary_path: PathInfo,
|
||||
pub(super) recovery_paths: BTreeMap<u16, PathInfo>,
|
||||
is_taproot: bool,
|
||||
}
|
||||
|
||||
impl LianaPolicy {
|
||||
/// Create a new Liana policy from a given configuration.
|
||||
pub fn new(
|
||||
fn _new(
|
||||
primary_path: PathInfo,
|
||||
recovery_paths: BTreeMap<u16, PathInfo>,
|
||||
is_taproot: bool,
|
||||
) -> Result<LianaPolicy, LianaPolicyError> {
|
||||
if recovery_paths.is_empty() {
|
||||
return Err(LianaPolicyError::MissingRecoveryPath);
|
||||
@ -430,32 +522,73 @@ impl LianaPolicy {
|
||||
let policy = LianaPolicy {
|
||||
primary_path,
|
||||
recovery_paths,
|
||||
is_taproot,
|
||||
};
|
||||
policy.clone().into_miniscript()?;
|
||||
policy.clone().into_multipath_descriptor_fallible()?;
|
||||
Ok(policy)
|
||||
}
|
||||
|
||||
/// Create a new Liana policy for use under a Taproot context.
|
||||
pub fn new(
|
||||
primary_path: PathInfo,
|
||||
recovery_paths: BTreeMap<u16, PathInfo>,
|
||||
) -> Result<LianaPolicy, LianaPolicyError> {
|
||||
Self::_new(primary_path, recovery_paths, /* is_taproot = */ true)
|
||||
}
|
||||
|
||||
/// Create a new Liana policy for use under a P2WSH context.
|
||||
pub fn new_legacy(
|
||||
primary_path: PathInfo,
|
||||
recovery_paths: BTreeMap<u16, PathInfo>,
|
||||
) -> Result<LianaPolicy, LianaPolicyError> {
|
||||
Self::_new(primary_path, recovery_paths, /* is_taproot = */ false)
|
||||
}
|
||||
|
||||
/// Create a Liana policy from a descriptor. This will check the descriptor is correctly formed
|
||||
/// (P2WSH, multipath, ..) and has a valid Liana semantic.
|
||||
pub fn from_multipath_descriptor(
|
||||
desc: &descriptor::Descriptor<descriptor::DescriptorPublicKey>,
|
||||
) -> Result<LianaPolicy, LianaPolicyError> {
|
||||
// For now we only allow P2WSH descriptors.
|
||||
let wsh_desc = match &desc {
|
||||
descriptor::Descriptor::Wsh(desc) => desc,
|
||||
_ => return Err(LianaPolicyError::IncompatibleDesc),
|
||||
};
|
||||
|
||||
// Lift a semantic policy out of this Miniscript and normalize it to make sure we compare
|
||||
// apples to apples below.
|
||||
let ms = match wsh_desc.as_inner() {
|
||||
descriptor::WshInner::Ms(ms) => ms,
|
||||
let policy = match desc {
|
||||
descriptor::Descriptor::Wsh(wsh_desc) => {
|
||||
let ms = match wsh_desc.as_inner() {
|
||||
descriptor::WshInner::Ms(ms) => ms,
|
||||
_ => return Err(LianaPolicyError::IncompatibleDesc),
|
||||
};
|
||||
ms.lift().map_err(LianaPolicyError::PolicyAnalysis)?
|
||||
}
|
||||
descriptor::Descriptor::Tr(desc) => {
|
||||
// For Taproot, make sure to not take the internal key into account in the semantic
|
||||
// policy if it's unspendable.
|
||||
if let Some(tree) = desc.tap_tree() {
|
||||
let tree_policy = tree.lift().map_err(LianaPolicyError::PolicyAnalysis)?;
|
||||
let unspend_int_xpub = unspendable_internal_xpub(desc)
|
||||
.ok_or(LianaPolicyError::IncompatibleDesc)?;
|
||||
let desc_int_xpub = get_multi_xkey(desc.internal_key())
|
||||
.ok_or(LianaPolicyError::IncompatibleDesc)?;
|
||||
if *desc_int_xpub == unspend_int_xpub {
|
||||
tree_policy
|
||||
} else {
|
||||
SemanticPolicy::Threshold(
|
||||
1,
|
||||
vec![
|
||||
SemanticPolicy::Key(desc.internal_key().clone()),
|
||||
tree_policy,
|
||||
],
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// A Liana descriptor must contain a timelocked path.
|
||||
return Err(LianaPolicyError::IncompatibleDesc);
|
||||
}
|
||||
}
|
||||
// We only allow P2WSH and Taproot descriptors.
|
||||
_ => return Err(LianaPolicyError::IncompatibleDesc),
|
||||
};
|
||||
let policy = ms
|
||||
.lift()
|
||||
.map_err(LianaPolicyError::PolicyAnalysis)?
|
||||
.normalized();
|
||||
}
|
||||
.normalized();
|
||||
let is_taproot = matches!(desc, descriptor::Descriptor::Tr(..));
|
||||
|
||||
// The policy must always be "1 of N spending paths" with at least an always-available
|
||||
// primary path with at least one key, and at least one timelocked recovery path with at
|
||||
@ -499,7 +632,7 @@ impl LianaPolicy {
|
||||
// Use the constructor for sanity checking the keys and the Miniscript policy. Note this
|
||||
// makes sure the recovery paths mapping isn't empty, too.
|
||||
let prim_path = primary_path.ok_or(LianaPolicyError::IncompatibleDesc)?;
|
||||
LianaPolicy::new(prim_path, recovery_paths)
|
||||
LianaPolicy::_new(prim_path, recovery_paths, is_taproot)
|
||||
}
|
||||
|
||||
pub fn primary_path(&self) -> &PathInfo {
|
||||
@ -513,15 +646,11 @@ impl LianaPolicy {
|
||||
&self.recovery_paths
|
||||
}
|
||||
|
||||
fn into_miniscript(
|
||||
self,
|
||||
) -> Result<
|
||||
miniscript::Miniscript<descriptor::DescriptorPublicKey, miniscript::Segwitv0>,
|
||||
LianaPolicyError,
|
||||
> {
|
||||
fn into_policy(self) -> miniscript::policy::Concrete<descriptor::DescriptorPublicKey> {
|
||||
let LianaPolicy {
|
||||
primary_path,
|
||||
recovery_paths,
|
||||
..
|
||||
} = self;
|
||||
|
||||
// Start with the primary spending path. We'll then or() all the recovery paths to it.
|
||||
@ -529,20 +658,76 @@ impl LianaPolicy {
|
||||
|
||||
// Incrementally create the top-level policy using all recovery paths.
|
||||
assert!(!recovery_paths.is_empty());
|
||||
let tl_policy =
|
||||
recovery_paths
|
||||
.into_iter()
|
||||
.fold(primary_keys, |tl_policy, (timelock, path_info)| {
|
||||
let timelock = ConcretePolicy::Older(Sequence::from_height(timelock));
|
||||
let keys = path_info.into_ms_policy();
|
||||
let recovery_branch = ConcretePolicy::And(vec![keys.into(), timelock.into()]);
|
||||
// We assume the larger the timelock the less likely a branch would be used.
|
||||
ConcretePolicy::Or(vec![(99, tl_policy.into()), (1, recovery_branch.into())])
|
||||
});
|
||||
recovery_paths
|
||||
.into_iter()
|
||||
.fold(primary_keys, |tl_policy, (timelock, path_info)| {
|
||||
let timelock = ConcretePolicy::Older(Sequence::from_height(timelock));
|
||||
let keys = path_info.into_ms_policy();
|
||||
let recovery_branch = ConcretePolicy::And(vec![keys.into(), timelock.into()]);
|
||||
// We assume the larger the timelock the less likely a branch would be used.
|
||||
ConcretePolicy::Or(vec![(99, tl_policy.into()), (1, recovery_branch.into())])
|
||||
})
|
||||
}
|
||||
|
||||
tl_policy
|
||||
.compile::<miniscript::Segwitv0>()
|
||||
.map_err(LianaPolicyError::InvalidPolicy)
|
||||
fn into_multipath_descriptor_fallible(
|
||||
self,
|
||||
) -> Result<descriptor::Descriptor<descriptor::DescriptorPublicKey>, LianaPolicyError> {
|
||||
if self.is_taproot {
|
||||
// If compiling to a Taproot descriptor and we can't have an internal key, we want to
|
||||
// compute a deterministic unspendable key to use as internal key. We compute it from
|
||||
// the xpubs in the Taptree as per
|
||||
// https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304/21. However, there
|
||||
// is clearly an inter-dependency here: we need an internal key to get the Taptree, and
|
||||
// vice-versa. So we use a dummy internal key. If it ends up as the internal key in the
|
||||
// compiled descriptor, we replace it with a deterministically computed unspendable
|
||||
// internal key.
|
||||
let dummy_internal_key =
|
||||
descriptor::DescriptorPublicKey::XPub(descriptor::DescriptorXKey::<bip32::Xpub> {
|
||||
origin: None,
|
||||
xkey: bip32::Xpub {
|
||||
public_key: bip341_nums(),
|
||||
chain_code: [0; 32].into(),
|
||||
depth: 0,
|
||||
parent_fingerprint: [0; 4].into(),
|
||||
child_number: 0.into(),
|
||||
network: bitcoin::Network::Regtest,
|
||||
},
|
||||
derivation_path: vec![].into(),
|
||||
wildcard: descriptor::Wildcard::None,
|
||||
});
|
||||
let policy = self.into_policy();
|
||||
let desc = policy
|
||||
.clone()
|
||||
.compile_tr(Some(dummy_internal_key.clone()))
|
||||
.map_err(LianaPolicyError::InvalidPolicy)?;
|
||||
let inner_desc = if let descriptor::Descriptor::Tr(ref d) = desc {
|
||||
d
|
||||
} else {
|
||||
unreachable!("compile_tr() always gives a tr() descriptor.");
|
||||
};
|
||||
if inner_desc.internal_key() == &dummy_internal_key {
|
||||
// Unfortunately to replace the dummy internal key with the correct one we need to
|
||||
// perform the computation again.
|
||||
let actual_internal_key = unspendable_internal_key(inner_desc)
|
||||
.expect("Desc has a Taptree and only multixpubs.");
|
||||
policy
|
||||
.compile_tr(Some(actual_internal_key))
|
||||
.map_err(LianaPolicyError::InvalidPolicy)
|
||||
} else {
|
||||
// A key from the policy could be used as internal key. No need for a deterministic
|
||||
// internal key.
|
||||
Ok(desc)
|
||||
}
|
||||
} else {
|
||||
let ms = self
|
||||
.into_policy()
|
||||
.compile::<miniscript::Segwitv0>()
|
||||
.map_err(|e| LianaPolicyError::InvalidPolicy(e.into()))?;
|
||||
miniscript::Segwitv0::check_local_validity(&ms).expect("Miniscript must be sane");
|
||||
Ok(descriptor::Descriptor::Wsh(
|
||||
descriptor::Wsh::new(ms).expect("Must pass sanity checks"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a descriptor from this spending policy with multipath key expressions. Note this
|
||||
@ -552,11 +737,8 @@ impl LianaPolicy {
|
||||
pub fn into_multipath_descriptor(
|
||||
self,
|
||||
) -> descriptor::Descriptor<descriptor::DescriptorPublicKey> {
|
||||
let ms = self
|
||||
.into_miniscript()
|
||||
.expect("This is always checked when creating a LianaPolicy.");
|
||||
miniscript::Segwitv0::check_local_validity(&ms).expect("Miniscript must be sane");
|
||||
descriptor::Descriptor::Wsh(descriptor::Wsh::new(ms).expect("Must pass sanity checks"))
|
||||
self.into_multipath_descriptor_fallible()
|
||||
.expect("This is always checked when creating a LianaPolicy.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -229,7 +229,10 @@ fn setup_bitcoind(
|
||||
.as_ref()
|
||||
.ok_or(StartupError::MissingBitcoindConfig)?;
|
||||
let bitcoind = BitcoinD::new(bitcoind_config, wo_path_str)?;
|
||||
bitcoind.node_sanity_checks(config.bitcoin_config.network)?;
|
||||
bitcoind.node_sanity_checks(
|
||||
config.bitcoin_config.network,
|
||||
config.main_descriptor.is_taproot(),
|
||||
)?;
|
||||
if fresh_data_dir {
|
||||
log::info!("Creating a new watchonly wallet on bitcoind.");
|
||||
bitcoind.create_watchonly_wallet(&config.main_descriptor)?;
|
||||
|
||||
608
src/signer.rs
608
src/signer.rs
@ -18,7 +18,8 @@ use miniscript::bitcoin::{
|
||||
bip32::{self, Error as Bip32Error},
|
||||
ecdsa,
|
||||
hashes::Hash,
|
||||
psbt::Psbt,
|
||||
key::TapTweak,
|
||||
psbt::{Input as PsbtIn, Psbt},
|
||||
secp256k1, sighash,
|
||||
};
|
||||
|
||||
@ -228,56 +229,167 @@ impl HotSigner {
|
||||
bip32::Xpub::from_priv(secp, &xpriv)
|
||||
}
|
||||
|
||||
// Provide an ECDSA signature for this transaction input from the PSBT input information.
|
||||
fn sign_p2wsh(
|
||||
&self,
|
||||
secp: &secp256k1::Secp256k1<impl secp256k1::Signing>,
|
||||
sighash_cache: &mut sighash::SighashCache<&bitcoin::Transaction>,
|
||||
master_fingerprint: bip32::Fingerprint,
|
||||
psbt_in: &mut PsbtIn,
|
||||
input_index: usize,
|
||||
) -> Result<(), SignerError> {
|
||||
// First of all compute the sighash for this input. We assume P2WSH spend: the sighash
|
||||
// script code is always the witness script.
|
||||
let witscript = psbt_in
|
||||
.witness_script
|
||||
.as_ref()
|
||||
.ok_or(SignerError::IncompletePsbt)?;
|
||||
let value = psbt_in
|
||||
.witness_utxo
|
||||
.as_ref()
|
||||
.ok_or(SignerError::IncompletePsbt)?
|
||||
.value;
|
||||
let sig_type = sighash::EcdsaSighashType::All;
|
||||
let sighash = sighash_cache
|
||||
.p2wsh_signature_hash(input_index, witscript, value, sig_type)
|
||||
.map_err(|_| SignerError::InsanePsbt)?;
|
||||
let sighash = secp256k1::Message::from_digest_slice(sighash.as_byte_array())
|
||||
.expect("Sighash is always 32 bytes.");
|
||||
|
||||
// Then provide a signature for all the keys they asked for.
|
||||
for (curr_pubkey, (fingerprint, der_path)) in psbt_in.bip32_derivation.iter() {
|
||||
if *fingerprint != master_fingerprint {
|
||||
continue;
|
||||
}
|
||||
let privkey = self.xpriv_at(der_path, secp).to_priv();
|
||||
let pubkey = privkey.public_key(secp);
|
||||
if pubkey.inner != *curr_pubkey {
|
||||
return Err(SignerError::InsanePsbt);
|
||||
}
|
||||
let sig = secp.sign_ecdsa_low_r(&sighash, &privkey.inner);
|
||||
psbt_in.partial_sigs.insert(
|
||||
pubkey,
|
||||
ecdsa::Signature {
|
||||
sig,
|
||||
hash_ty: sig_type,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Provide a BIP340 signature for this transaction input from the PSBT input information.
|
||||
fn sign_taproot(
|
||||
&self,
|
||||
secp: &secp256k1::Secp256k1<secp256k1::All>,
|
||||
sighash_cache: &mut sighash::SighashCache<&bitcoin::Transaction>,
|
||||
master_fingerprint: bip32::Fingerprint,
|
||||
prevouts: &[bitcoin::TxOut],
|
||||
psbt_in: &mut PsbtIn,
|
||||
input_index: usize,
|
||||
) -> Result<(), SignerError> {
|
||||
let sig_type = sighash::TapSighashType::Default;
|
||||
let prevouts = sighash::Prevouts::All(prevouts);
|
||||
|
||||
// If the details of the internal key are filled, provide a keypath signature.
|
||||
if let Some(ref int_key) = psbt_in.tap_internal_key {
|
||||
// NB: we don't check for empty leaf hashes on purpose, in case the internal key also
|
||||
// appears in a leaf.
|
||||
if let Some((_, (fg, der_path))) = psbt_in.tap_key_origins.get(int_key) {
|
||||
if *fg == master_fingerprint {
|
||||
let privkey = self.xpriv_at(der_path, secp).to_priv();
|
||||
let keypair = secp256k1::Keypair::from_secret_key(secp, &privkey.inner);
|
||||
if keypair.x_only_public_key().0 != *int_key {
|
||||
return Err(SignerError::InsanePsbt);
|
||||
}
|
||||
let keypair = keypair.tap_tweak(secp, psbt_in.tap_merkle_root).to_inner();
|
||||
let sighash = sighash_cache
|
||||
.taproot_key_spend_signature_hash(input_index, &prevouts, sig_type)
|
||||
.map_err(|_| SignerError::InsanePsbt)?;
|
||||
let sighash = secp256k1::Message::from_digest_slice(sighash.as_byte_array())
|
||||
.expect("Sighash is always 32 bytes.");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&sighash, &keypair);
|
||||
let sig = bitcoin::taproot::Signature {
|
||||
sig,
|
||||
hash_ty: sig_type,
|
||||
};
|
||||
psbt_in.tap_key_sig = Some(sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now sign for all the public keys derived from our master secret, in all the leaves where
|
||||
// they are present.
|
||||
for (pubkey, (leaf_hashes, (fg, der_path))) in &psbt_in.tap_key_origins {
|
||||
if *fg != master_fingerprint {
|
||||
continue;
|
||||
}
|
||||
|
||||
for leaf_hash in leaf_hashes {
|
||||
let privkey = self.xpriv_at(der_path, secp).to_priv();
|
||||
let keypair = secp256k1::Keypair::from_secret_key(secp, &privkey.inner);
|
||||
let sighash = sighash_cache
|
||||
.taproot_script_spend_signature_hash(
|
||||
input_index,
|
||||
&prevouts,
|
||||
*leaf_hash,
|
||||
sig_type,
|
||||
)
|
||||
.map_err(|_| SignerError::InsanePsbt)?;
|
||||
let sighash = secp256k1::Message::from_digest_slice(sighash.as_byte_array())
|
||||
.expect("Sighash is always 32 bytes.");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&sighash, &keypair);
|
||||
let sig = bitcoin::taproot::Signature {
|
||||
sig,
|
||||
hash_ty: sig_type,
|
||||
};
|
||||
psbt_in.tap_script_sigs.insert((*pubkey, *leaf_hash), sig);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sign all inputs of the given PSBT.
|
||||
///
|
||||
/// **This does not perform any check. It will blindly sign anything that's passed.**
|
||||
pub fn sign_psbt(
|
||||
&self,
|
||||
mut psbt: Psbt,
|
||||
secp: &secp256k1::Secp256k1<impl secp256k1::Signing>,
|
||||
secp: &secp256k1::Secp256k1<secp256k1::All>,
|
||||
) -> Result<Psbt, SignerError> {
|
||||
let master_fingerprint = self.fingerprint(secp);
|
||||
let mut sighash_cache = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||
|
||||
let prevouts: Vec<_> = psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|psbt_in| psbt_in.witness_utxo.clone())
|
||||
.collect();
|
||||
if prevouts.len() != psbt.inputs.len() {
|
||||
return Err(SignerError::IncompletePsbt);
|
||||
}
|
||||
|
||||
// Sign each input in the PSBT.
|
||||
for i in 0..psbt.inputs.len() {
|
||||
// First of all compute the sighash for this input. We assume P2WSH spend: the sighash
|
||||
// script code is always the witness script.
|
||||
let witscript = psbt.inputs[i]
|
||||
.witness_script
|
||||
.as_ref()
|
||||
.ok_or(SignerError::IncompletePsbt)?;
|
||||
let value = psbt.inputs[i]
|
||||
.witness_utxo
|
||||
.as_ref()
|
||||
.ok_or(SignerError::IncompletePsbt)?
|
||||
.value;
|
||||
let sig_type = sighash::EcdsaSighashType::All;
|
||||
let sighash = sighash_cache
|
||||
.p2wsh_signature_hash(i, witscript, value, sig_type)
|
||||
.map_err(|_| SignerError::InsanePsbt)?;
|
||||
let sighash = secp256k1::Message::from_digest_slice(sighash.as_byte_array())
|
||||
.expect("Sighash is always 32 bytes.");
|
||||
|
||||
// Then provide a signature for all the keys they asked for.
|
||||
let input = &mut psbt.inputs[i]; // for borrowck reasons
|
||||
for (curr_pubkey, (fingerprint, der_path)) in input.bip32_derivation.iter() {
|
||||
if *fingerprint != master_fingerprint {
|
||||
continue;
|
||||
}
|
||||
let privkey = self.xpriv_at(der_path, secp).to_priv();
|
||||
let pubkey = privkey.public_key(secp);
|
||||
if pubkey.inner != *curr_pubkey {
|
||||
return Err(SignerError::InsanePsbt);
|
||||
}
|
||||
let sig = secp.sign_ecdsa_low_r(&sighash, &privkey.inner);
|
||||
input.partial_sigs.insert(
|
||||
pubkey,
|
||||
ecdsa::Signature {
|
||||
sig,
|
||||
hash_ty: sig_type,
|
||||
},
|
||||
);
|
||||
if psbt.inputs[i].witness_script.is_some() {
|
||||
self.sign_p2wsh(
|
||||
secp,
|
||||
&mut sighash_cache,
|
||||
master_fingerprint,
|
||||
&mut psbt.inputs[i],
|
||||
i,
|
||||
)?;
|
||||
} else {
|
||||
self.sign_taproot(
|
||||
secp,
|
||||
&mut sighash_cache,
|
||||
master_fingerprint,
|
||||
&prevouts,
|
||||
&mut psbt.inputs[i],
|
||||
i,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,7 +469,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hot_signer_sign() {
|
||||
fn hot_signer_sign_p2wsh() {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
let network = bitcoin::Network::Bitcoin;
|
||||
|
||||
@ -419,14 +531,22 @@ mod tests {
|
||||
wildcard: Wildcard::Unhardened,
|
||||
});
|
||||
let recov_keys = descriptors::PathInfo::Single(recov_key);
|
||||
let policy =
|
||||
descriptors::LianaPolicy::new(prim_keys, [(46, recov_keys)].iter().cloned().collect())
|
||||
.unwrap();
|
||||
let policy = descriptors::LianaPolicy::new_legacy(
|
||||
prim_keys,
|
||||
[(46, recov_keys)].iter().cloned().collect(),
|
||||
)
|
||||
.unwrap();
|
||||
let desc = descriptors::LianaDescriptor::new(policy);
|
||||
|
||||
// Create a dummy PSBT spending a coin from this descriptor with a single input and single
|
||||
// (external) output. We'll be modifying it as we go.
|
||||
let spent_coin_desc = desc.receive_descriptor().derive(42.into(), &secp);
|
||||
let mut psbt_in = PsbtIn::default();
|
||||
spent_coin_desc.update_psbt_in(&mut psbt_in);
|
||||
psbt_in.witness_utxo = Some(bitcoin::TxOut {
|
||||
value: Amount::from_sat(19_000),
|
||||
script_pubkey: spent_coin_desc.script_pubkey(),
|
||||
});
|
||||
let mut dummy_psbt = Psbt {
|
||||
unsigned_tx: bitcoin::Transaction {
|
||||
version: bitcoin::transaction::Version::TWO,
|
||||
@ -453,15 +573,7 @@ mod tests {
|
||||
xpub: BTreeMap::new(),
|
||||
proprietary: BTreeMap::new(),
|
||||
unknown: BTreeMap::new(),
|
||||
inputs: vec![PsbtIn {
|
||||
witness_script: Some(spent_coin_desc.witness_script()),
|
||||
bip32_derivation: spent_coin_desc.bip32_derivations(),
|
||||
witness_utxo: Some(bitcoin::TxOut {
|
||||
value: Amount::from_sat(19_000),
|
||||
script_pubkey: spent_coin_desc.script_pubkey(),
|
||||
}),
|
||||
..PsbtIn::default()
|
||||
}],
|
||||
inputs: vec![psbt_in],
|
||||
outputs: Vec::new(),
|
||||
};
|
||||
|
||||
@ -469,12 +581,20 @@ mod tests {
|
||||
// that it manages.
|
||||
let psbt = dummy_psbt.clone();
|
||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 1);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 2);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 4);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
|
||||
// We can add another external output to the transaction, we can still sign without issue.
|
||||
// The output can be insane, we don't check it. It doesn't even need an accompanying PSBT
|
||||
@ -482,25 +602,31 @@ mod tests {
|
||||
dummy_psbt.unsigned_tx.output.push(bitcoin::TxOut::NULL);
|
||||
let psbt = dummy_psbt.clone();
|
||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 1);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 2);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 4);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
|
||||
// We can add another input to the PSBT. If we don't attach also another transaction input
|
||||
// it will fail.
|
||||
let other_spent_coin_desc = desc.receive_descriptor().derive(84.into(), &secp);
|
||||
dummy_psbt.inputs.push(PsbtIn {
|
||||
witness_script: Some(other_spent_coin_desc.witness_script()),
|
||||
bip32_derivation: other_spent_coin_desc.bip32_derivations(),
|
||||
witness_utxo: Some(bitcoin::TxOut {
|
||||
value: Amount::from_sat(19_000),
|
||||
script_pubkey: other_spent_coin_desc.script_pubkey(),
|
||||
}),
|
||||
..PsbtIn::default()
|
||||
let mut psbt_in = PsbtIn::default();
|
||||
other_spent_coin_desc.update_psbt_in(&mut psbt_in);
|
||||
psbt_in.witness_utxo = Some(bitcoin::TxOut {
|
||||
value: Amount::from_sat(19_000),
|
||||
script_pubkey: other_spent_coin_desc.script_pubkey(),
|
||||
});
|
||||
dummy_psbt.inputs.push(psbt_in);
|
||||
let psbt = dummy_psbt.clone();
|
||||
assert!(prim_signer_a
|
||||
.sign_psbt(psbt, &secp)
|
||||
@ -524,31 +650,63 @@ mod tests {
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.partial_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_none()));
|
||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.partial_sigs.len() == 1));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_none()));
|
||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.partial_sigs.len() == 2));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_none()));
|
||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.partial_sigs.len() == 4));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_none()));
|
||||
|
||||
// If the witness script is missing for one of the inputs it'll tell us the PSBT is
|
||||
// incomplete.
|
||||
// If the witness script is missing for one of the inputs it'll assume it's a Taproot input
|
||||
// and provide Taproot signatures. But since we haven't provided any Taproot details it
|
||||
// won't fill anything.
|
||||
let mut psbt = dummy_psbt.clone();
|
||||
psbt.inputs[1].witness_script = None;
|
||||
assert!(prim_signer_a
|
||||
.sign_psbt(psbt, &secp)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("The PSBT is missing some information necessary for signing."));
|
||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt.inputs[1].partial_sigs.is_empty());
|
||||
assert!(psbt.inputs[1].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[1].tap_script_sigs.is_empty());
|
||||
|
||||
// If the witness utxo is missing for one of the inputs it'll tell us the PSBT is
|
||||
// incomplete.
|
||||
@ -571,6 +729,322 @@ mod tests {
|
||||
assert_eq!(psbt.inputs[1].partial_sigs.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hot_signer_sign_taproot() {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
let network = bitcoin::Network::Bitcoin;
|
||||
|
||||
// Create a Liana descriptor with as primary path a 2-of-3 with three hot signers and a
|
||||
// single hot signer as recovery path. (The recovery path signer is also used in the
|
||||
// primary path.) Use various random derivation paths.
|
||||
let (prim_signer_a, prim_signer_b, recov_signer) = (
|
||||
HotSigner::generate(network).unwrap(),
|
||||
HotSigner::generate(network).unwrap(),
|
||||
HotSigner::generate(network).unwrap(),
|
||||
);
|
||||
let origin_der = bip32::DerivationPath::from_str("m/0'/12'/42").unwrap();
|
||||
let xkey = prim_signer_a.xpub_at(&origin_der, &secp);
|
||||
let prim_key_a = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
|
||||
origin: Some((prim_signer_a.fingerprint(&secp), origin_der)),
|
||||
xkey,
|
||||
derivation_paths: DerivPaths::new(vec![
|
||||
bip32::DerivationPath::from_str("m/420/56/0").unwrap(),
|
||||
bip32::DerivationPath::from_str("m/420/56/1").unwrap(),
|
||||
])
|
||||
.unwrap(),
|
||||
wildcard: Wildcard::Unhardened,
|
||||
});
|
||||
let origin_der = bip32::DerivationPath::from_str("m/18'/24'").unwrap();
|
||||
let xkey = prim_signer_b.xpub_at(&origin_der, &secp);
|
||||
let prim_key_b = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
|
||||
origin: Some((prim_signer_b.fingerprint(&secp), origin_der)),
|
||||
xkey,
|
||||
derivation_paths: DerivPaths::new(vec![
|
||||
bip32::DerivationPath::from_str("m/31/0").unwrap(),
|
||||
bip32::DerivationPath::from_str("m/31/1").unwrap(),
|
||||
])
|
||||
.unwrap(),
|
||||
wildcard: Wildcard::Unhardened,
|
||||
});
|
||||
let origin_der = bip32::DerivationPath::from_str("m/18'/25'").unwrap();
|
||||
let xkey = recov_signer.xpub_at(&origin_der, &secp);
|
||||
let prim_key_c = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
|
||||
origin: Some((recov_signer.fingerprint(&secp), origin_der)),
|
||||
xkey,
|
||||
derivation_paths: DerivPaths::new(vec![
|
||||
bip32::DerivationPath::from_str("m/0").unwrap(),
|
||||
bip32::DerivationPath::from_str("m/1").unwrap(),
|
||||
])
|
||||
.unwrap(),
|
||||
wildcard: Wildcard::Unhardened,
|
||||
});
|
||||
let prim_keys =
|
||||
descriptors::PathInfo::Multi(2, vec![prim_key_a.clone(), prim_key_b, prim_key_c]);
|
||||
let origin_der = bip32::DerivationPath::from_str("m/1/2'/3/4'").unwrap();
|
||||
let xkey = recov_signer.xpub_at(&origin_der, &secp);
|
||||
let recov_key = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
|
||||
origin: Some((recov_signer.fingerprint(&secp), origin_der)),
|
||||
xkey,
|
||||
derivation_paths: DerivPaths::new(vec![
|
||||
bip32::DerivationPath::from_str("m/5/6/0").unwrap(),
|
||||
bip32::DerivationPath::from_str("m/5/6/1").unwrap(),
|
||||
])
|
||||
.unwrap(),
|
||||
wildcard: Wildcard::Unhardened,
|
||||
});
|
||||
let recov_keys = descriptors::PathInfo::Single(recov_key.clone());
|
||||
let policy =
|
||||
descriptors::LianaPolicy::new(prim_keys, [(46, recov_keys)].iter().cloned().collect())
|
||||
.unwrap();
|
||||
let desc = descriptors::LianaDescriptor::new(policy);
|
||||
|
||||
// Create a dummy PSBT spending a coin from this descriptor with a single input and single
|
||||
// (external) output. We'll be modifying it as we go.
|
||||
let spent_coin_desc = desc.receive_descriptor().derive(42.into(), &secp);
|
||||
let mut psbt_in = PsbtIn::default();
|
||||
spent_coin_desc.update_psbt_in(&mut psbt_in);
|
||||
psbt_in.witness_utxo = Some(bitcoin::TxOut {
|
||||
value: Amount::from_sat(19_000),
|
||||
script_pubkey: spent_coin_desc.script_pubkey(),
|
||||
});
|
||||
let mut dummy_psbt = Psbt {
|
||||
unsigned_tx: bitcoin::Transaction {
|
||||
version: bitcoin::transaction::Version::TWO,
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO),
|
||||
input: vec![bitcoin::TxIn {
|
||||
sequence: bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME,
|
||||
previous_output: bitcoin::OutPoint::from_str(
|
||||
"4613e078e4cdbb0fce1bc6e44b028f0e11621a134a1605efdc456c32d155c922:19",
|
||||
)
|
||||
.unwrap(),
|
||||
..bitcoin::TxIn::default()
|
||||
}],
|
||||
output: vec![bitcoin::TxOut {
|
||||
value: Amount::from_sat(18_420),
|
||||
script_pubkey: bitcoin::Address::from_str(
|
||||
"bc1qvklensptw5lk7d470ds60pcpsr0psdpgyvwepv",
|
||||
)
|
||||
.unwrap()
|
||||
.payload()
|
||||
.script_pubkey(),
|
||||
}],
|
||||
},
|
||||
version: 0,
|
||||
xpub: BTreeMap::new(),
|
||||
proprietary: BTreeMap::new(),
|
||||
unknown: BTreeMap::new(),
|
||||
inputs: vec![psbt_in],
|
||||
outputs: Vec::new(),
|
||||
};
|
||||
|
||||
// Sign the PSBT with the two primary signers. The recovery signer will sign for the two keys
|
||||
// that it manages.
|
||||
let psbt = dummy_psbt.clone();
|
||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].tap_script_sigs.len(), 1);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].tap_script_sigs.len(), 2);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].tap_script_sigs.len(), 4);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||
|
||||
// We can add another external output to the transaction, we can still sign without issue.
|
||||
// The output can be insane, we don't check it. It doesn't even need an accompanying PSBT
|
||||
// output.
|
||||
dummy_psbt.unsigned_tx.output.push(bitcoin::TxOut::NULL);
|
||||
let psbt = dummy_psbt.clone();
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].tap_script_sigs.len(), 1);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].tap_script_sigs.len(), 2);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||
assert_eq!(psbt.inputs[0].tap_script_sigs.len(), 4);
|
||||
assert!(psbt.inputs[0].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||
|
||||
// We can add another input to the PSBT. If we don't attach also another transaction input
|
||||
// it will fail.
|
||||
let other_spent_coin_desc = desc.receive_descriptor().derive(84.into(), &secp);
|
||||
let mut psbt_in = PsbtIn::default();
|
||||
other_spent_coin_desc.update_psbt_in(&mut psbt_in);
|
||||
psbt_in.witness_utxo = Some(bitcoin::TxOut {
|
||||
value: Amount::from_sat(19_000),
|
||||
script_pubkey: other_spent_coin_desc.script_pubkey(),
|
||||
});
|
||||
dummy_psbt.inputs.push(psbt_in);
|
||||
let psbt = dummy_psbt.clone();
|
||||
assert!(prim_signer_a
|
||||
.sign_psbt(psbt, &secp)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Information contained in the PSBT is wrong"));
|
||||
|
||||
// But now if we add the inputs also to the transaction itself, it will have signed both
|
||||
// inputs.
|
||||
dummy_psbt.unsigned_tx.input.push(bitcoin::TxIn {
|
||||
// Note the sequence can be different. We don't care.
|
||||
sequence: bitcoin::Sequence::ENABLE_LOCKTIME_NO_RBF,
|
||||
previous_output: bitcoin::OutPoint::from_str(
|
||||
"5613e078e4cdbb0fce1bc6e44b028f0e11621a134a1605efdc456c32d155c922:0",
|
||||
)
|
||||
.unwrap(),
|
||||
..bitcoin::TxIn::default()
|
||||
});
|
||||
let psbt = dummy_psbt.clone();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_none()));
|
||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.len() == 1));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.partial_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_none()));
|
||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.len() == 2));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.partial_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_none()));
|
||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.len() == 4));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.partial_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_none()));
|
||||
|
||||
// If the witness script is set it'll assume it's a P2WSH input and provide ECDSA sigs.
|
||||
// But since we haven't provided any P2WSH details it won't fill anything.
|
||||
let mut psbt = dummy_psbt.clone();
|
||||
psbt.inputs[1].witness_script = Some(Default::default());
|
||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt.inputs[1].partial_sigs.is_empty());
|
||||
assert!(psbt.inputs[1].tap_key_sig.is_none());
|
||||
assert!(psbt.inputs[1].tap_script_sigs.is_empty());
|
||||
|
||||
// If the witness utxo is missing for one of the inputs it'll tell us the PSBT is
|
||||
// incomplete.
|
||||
let mut psbt = dummy_psbt.clone();
|
||||
psbt.inputs[1].witness_utxo = None;
|
||||
assert!(prim_signer_a
|
||||
.sign_psbt(psbt, &secp)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("The PSBT is missing some information necessary for signing."));
|
||||
|
||||
// If we remove the BIP32 derivations for the first input it will only provide signatures
|
||||
// for the second one.
|
||||
let mut psbt = dummy_psbt.clone();
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
assert!(psbt.inputs[1].tap_script_sigs.is_empty());
|
||||
psbt.inputs[0].tap_key_origins.clear();
|
||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt.inputs[0].tap_script_sigs.is_empty());
|
||||
assert_eq!(psbt.inputs[1].tap_script_sigs.len(), 1);
|
||||
|
||||
// Now use a Taproot descriptor such as there is a single primary key as the internal key.
|
||||
let prim_keys = descriptors::PathInfo::Single(prim_key_a);
|
||||
let recov_keys = descriptors::PathInfo::Single(recov_key);
|
||||
let policy =
|
||||
descriptors::LianaPolicy::new(prim_keys, [(42, recov_keys)].iter().cloned().collect())
|
||||
.unwrap();
|
||||
let desc = descriptors::LianaDescriptor::new(policy);
|
||||
let spent_coin_desc = desc.receive_descriptor().derive(412.into(), &secp);
|
||||
|
||||
// Update the two inputs with the details for this descriptor.
|
||||
dummy_psbt.inputs[0].tap_key_origins.clear();
|
||||
spent_coin_desc.update_psbt_in(&mut dummy_psbt.inputs[0]);
|
||||
dummy_psbt.inputs[1].tap_key_origins.clear();
|
||||
spent_coin_desc.update_psbt_in(&mut dummy_psbt.inputs[1]);
|
||||
|
||||
// Sign the PSBT with the primary and recovery signers. The prim signer will add a sig for
|
||||
// the key path and the recov signer for the script path.
|
||||
let psbt = dummy_psbt.clone();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_none()));
|
||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_some()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.is_empty()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.partial_sigs.is_empty()));
|
||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_key_sig.is_some()));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.tap_script_sigs.len() == 1));
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|psbt_in| psbt_in.partial_sigs.is_empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signer_set_net() {
|
||||
let secp = secp256k1::Secp256k1::signing_only();
|
||||
|
||||
50
src/spend.rs
50
src/spend.rs
@ -114,7 +114,7 @@ fn sanity_check_psbt(
|
||||
// index set for signing devices to recognize them as ours.
|
||||
let mut value_in = 0;
|
||||
for psbtin in psbt.inputs.iter() {
|
||||
if psbtin.bip32_derivation.is_empty() {
|
||||
if psbtin.bip32_derivation.is_empty() && psbtin.tap_key_origins.is_empty() {
|
||||
return Err(SpendCreationError::SanityCheckFailure(psbt.clone()));
|
||||
}
|
||||
value_in += psbtin
|
||||
@ -649,20 +649,17 @@ pub fn create_spend(
|
||||
});
|
||||
// If it's an address of ours, signal it as change to signing devices by adding the
|
||||
// BIP32 derivation path to the PSBT output.
|
||||
let bip32_derivation = if let Some(AddrInfo { index, is_change }) = address.info {
|
||||
let mut psbt_out = PsbtOut::default();
|
||||
if let Some(AddrInfo { index, is_change }) = address.info {
|
||||
let desc = if is_change {
|
||||
main_descriptor.change_descriptor()
|
||||
} else {
|
||||
main_descriptor.receive_descriptor()
|
||||
};
|
||||
desc.derive(index, secp).bip32_derivations()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
psbt_outs.push(PsbtOut {
|
||||
bip32_derivation,
|
||||
..PsbtOut::default()
|
||||
});
|
||||
desc.derive(index, secp)
|
||||
.update_change_psbt_out(&mut psbt_out)
|
||||
}
|
||||
psbt_outs.push(psbt_out);
|
||||
}
|
||||
assert_eq!(tx.output.is_empty(), is_self_send);
|
||||
|
||||
@ -718,24 +715,21 @@ pub fn create_spend(
|
||||
|
||||
// If the change address is ours, tell the signers by setting the BIP32 derivations in the
|
||||
// PSBT output.
|
||||
let bip32_derivation = if let Some(AddrInfo { index, is_change }) = change_addr.info {
|
||||
let mut psbt_out = PsbtOut::default();
|
||||
if let Some(AddrInfo { index, is_change }) = change_addr.info {
|
||||
let desc = if is_change {
|
||||
main_descriptor.change_descriptor()
|
||||
} else {
|
||||
main_descriptor.receive_descriptor()
|
||||
};
|
||||
desc.derive(index, secp).bip32_derivations()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
desc.derive(index, secp)
|
||||
.update_change_psbt_out(&mut psbt_out);
|
||||
}
|
||||
|
||||
// TODO: shuffle once we have Taproot
|
||||
change_txo.value = change_amount;
|
||||
tx.output.push(change_txo);
|
||||
psbt_outs.push(PsbtOut {
|
||||
bip32_derivation,
|
||||
..PsbtOut::default()
|
||||
});
|
||||
psbt_outs.push(psbt_out);
|
||||
} else if max_change_amount.to_sat() > 0 {
|
||||
warnings.push(CreateSpendWarning::ChangeAddedToFee(
|
||||
max_change_amount.to_sat(),
|
||||
@ -762,21 +756,17 @@ pub fn create_spend(
|
||||
});
|
||||
|
||||
// Populate the PSBT input with the information needed by signers.
|
||||
let mut psbt_in = PsbtIn::default();
|
||||
let coin_desc = derived_desc(secp, main_descriptor, cand);
|
||||
let witness_script = Some(coin_desc.witness_script());
|
||||
let witness_utxo = Some(bitcoin::TxOut {
|
||||
coin_desc.update_psbt_in(&mut psbt_in);
|
||||
psbt_in.witness_utxo = Some(bitcoin::TxOut {
|
||||
value: cand.amount,
|
||||
script_pubkey: coin_desc.script_pubkey(),
|
||||
});
|
||||
let non_witness_utxo = tx_getter.get_tx(&cand.outpoint.txid);
|
||||
let bip32_derivation = coin_desc.bip32_derivations();
|
||||
psbt_ins.push(PsbtIn {
|
||||
witness_script,
|
||||
witness_utxo,
|
||||
bip32_derivation,
|
||||
non_witness_utxo,
|
||||
..PsbtIn::default()
|
||||
});
|
||||
if !main_descriptor.is_taproot() {
|
||||
psbt_in.non_witness_utxo = tx_getter.get_tx(&cand.outpoint.txid);
|
||||
}
|
||||
psbt_ins.push(psbt_in);
|
||||
}
|
||||
|
||||
// Finally, create the PSBT with all inputs and outputs, sanity check it and return it.
|
||||
|
||||
@ -481,7 +481,7 @@ impl DummyLiana {
|
||||
|
||||
let owner_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap());
|
||||
let heir_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap());
|
||||
let policy = descriptors::LianaPolicy::new(
|
||||
let policy = descriptors::LianaPolicy::new_legacy(
|
||||
owner_key,
|
||||
[(10_000, heir_key)].iter().cloned().collect(),
|
||||
)
|
||||
|
||||
@ -44,6 +44,16 @@ From the root of the repository:
|
||||
pytest tests/
|
||||
```
|
||||
|
||||
For running the tests under Taproot a `bitcoind` version 26.0 or superior must be used. It can be
|
||||
pointed to using the `BITCOIND_PATH` variable. For now, one must also compile the `taproot_signer`
|
||||
Rust program:
|
||||
```
|
||||
(cd tests/tools/taproot_signer && cargo build --release)
|
||||
```
|
||||
|
||||
Then the test suite can be run by using Taproot descriptors instead of P2WSH descriptors by setting
|
||||
the `USE_TAPROOT` environment variable to `1`.
|
||||
|
||||
### Tips and tricks
|
||||
#### Logging
|
||||
|
||||
|
||||
@ -5,10 +5,9 @@ from concurrent import futures
|
||||
from test_framework.bitcoind import Bitcoind
|
||||
from test_framework.lianad import Lianad
|
||||
from test_framework.signer import SingleSigner, MultiSigner
|
||||
from test_framework.utils import (
|
||||
EXECUTOR_WORKERS,
|
||||
)
|
||||
from test_framework.utils import EXECUTOR_WORKERS, USE_TAPROOT
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import pytest
|
||||
import shutil
|
||||
@ -120,22 +119,36 @@ def xpub_fingerprint(hd):
|
||||
return _pubkey_to_fingerprint(hd.pubkey).hex()
|
||||
|
||||
|
||||
def single_key_desc(prim_fg, prim_xpub, reco_fg, reco_xpub, csv_value, is_taproot):
|
||||
if is_taproot:
|
||||
return f"tr([{prim_fg}]{prim_xpub}/<0;1>/*,and_v(v:pk([{reco_fg}]{reco_xpub}/<0;1>/*),older({csv_value})))"
|
||||
else:
|
||||
return f"wsh(or_d(pk([{prim_fg}]{prim_xpub}/<0;1>/*),and_v(v:pkh([{reco_fg}]{reco_xpub}/<0;1>/*),older({csv_value}))))"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lianad(bitcoind, directory):
|
||||
datadir = os.path.join(directory, "lianad")
|
||||
os.makedirs(datadir, exist_ok=True)
|
||||
bitcoind_cookie = os.path.join(bitcoind.bitcoin_dir, "regtest", ".cookie")
|
||||
|
||||
signer = SingleSigner()
|
||||
signer = SingleSigner(is_taproot=USE_TAPROOT)
|
||||
(prim_fingerprint, primary_xpub), (reco_fingerprint, recovery_xpub) = (
|
||||
(xpub_fingerprint(signer.primary_hd), signer.primary_hd.get_xpub()),
|
||||
(xpub_fingerprint(signer.recovery_hd), signer.recovery_hd.get_xpub()),
|
||||
)
|
||||
csv_value = 10
|
||||
# NOTE: origins are the actual xpub themselves which is incorrect but make it
|
||||
# NOTE: origins are the actual xpub themselves which is incorrect but makes it
|
||||
# possible to differentiate them.
|
||||
main_desc = Descriptor.from_str(
|
||||
f"wsh(or_d(pk([{prim_fingerprint}]{primary_xpub}/<0;1>/*),and_v(v:pkh([{reco_fingerprint}]{recovery_xpub}/<0;1>/*),older({csv_value}))))"
|
||||
single_key_desc(
|
||||
prim_fingerprint,
|
||||
primary_xpub,
|
||||
reco_fingerprint,
|
||||
recovery_xpub,
|
||||
csv_value,
|
||||
is_taproot=USE_TAPROOT,
|
||||
)
|
||||
)
|
||||
|
||||
lianad = Lianad(
|
||||
@ -156,8 +169,19 @@ def lianad(bitcoind, directory):
|
||||
lianad.cleanup()
|
||||
|
||||
|
||||
def multi_expression(thresh, keys):
|
||||
exp = f"multi({thresh},"
|
||||
def unspendable_internal_xpub(xpubs):
|
||||
"""Deterministic, unique, unspendable internal key.
|
||||
See See https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304/21
|
||||
"""
|
||||
chaincode = hashlib.sha256(b"".join(xpub.pubkey for xpub in xpubs)).digest()
|
||||
bip341_nums = bytes.fromhex(
|
||||
"0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
|
||||
)
|
||||
return BIP32(chaincode, pubkey=bip341_nums, network="test")
|
||||
|
||||
|
||||
def multi_expression(thresh, keys, is_taproot):
|
||||
exp = f"multi_a({thresh}," if is_taproot else f"multi({thresh},"
|
||||
for i, key in enumerate(keys):
|
||||
# NOTE: origins are the actual xpub themselves which is incorrect but make it
|
||||
# possible to differentiate them.
|
||||
@ -168,6 +192,21 @@ def multi_expression(thresh, keys):
|
||||
return exp + ")"
|
||||
|
||||
|
||||
def multisig_desc(multi_signer, csv_value, is_taproot):
|
||||
prim_multi, recov_multi = (
|
||||
multi_expression(3, multi_signer.prim_hds, is_taproot),
|
||||
multi_expression(2, multi_signer.recov_hds[csv_value], is_taproot),
|
||||
)
|
||||
if is_taproot:
|
||||
all_xpubs = [
|
||||
hd for hd in multi_signer.prim_hds + multi_signer.recov_hds[csv_value]
|
||||
]
|
||||
internal_key = unspendable_internal_xpub(all_xpubs).get_xpub()
|
||||
return f"tr([00000000]{internal_key}/<0;1>/*,{{{prim_multi},and_v(v:{recov_multi},older({csv_value}))}})"
|
||||
else:
|
||||
return f"wsh(or_d({prim_multi},and_v(v:{recov_multi},older({csv_value}))))"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lianad_multisig(bitcoind, directory):
|
||||
datadir = os.path.join(directory, "lianad")
|
||||
@ -176,13 +215,9 @@ def lianad_multisig(bitcoind, directory):
|
||||
|
||||
# A 3-of-4 that degrades into a 2-of-5 after 10 blocks
|
||||
csv_value = 10
|
||||
signer = MultiSigner(4, {csv_value: 5})
|
||||
prim_multi, recov_multi = (
|
||||
multi_expression(3, signer.prim_hds),
|
||||
multi_expression(2, signer.recov_hds[csv_value]),
|
||||
)
|
||||
signer = MultiSigner(4, {csv_value: 5}, is_taproot=USE_TAPROOT)
|
||||
main_desc = Descriptor.from_str(
|
||||
f"wsh(or_d({prim_multi},and_v(v:{recov_multi},older({csv_value}))))"
|
||||
multisig_desc(signer, csv_value, is_taproot=USE_TAPROOT)
|
||||
)
|
||||
|
||||
lianad = Lianad(
|
||||
@ -203,6 +238,28 @@ def lianad_multisig(bitcoind, directory):
|
||||
lianad.cleanup()
|
||||
|
||||
|
||||
def multipath_desc(multi_signer, csv_values, is_taproot):
|
||||
prim_multi = multi_expression(3, multi_signer.prim_hds, is_taproot)
|
||||
first_recov_multi = multi_expression(
|
||||
3, multi_signer.recov_hds[csv_values[0]], is_taproot
|
||||
)
|
||||
second_recov_multi = multi_expression(
|
||||
1, multi_signer.recov_hds[csv_values[1]], is_taproot
|
||||
)
|
||||
if is_taproot:
|
||||
all_xpubs = [
|
||||
hd
|
||||
for hd in multi_signer.prim_hds
|
||||
+ multi_signer.recov_hds[csv_values[0]]
|
||||
+ multi_signer.recov_hds[csv_values[1]]
|
||||
]
|
||||
internal_key = unspendable_internal_xpub(all_xpubs).get_xpub()
|
||||
# On purpose we use a single leaf instead of 3 different ones. It shouldn't be an issue.
|
||||
return f"tr([00000000]{internal_key}/<0;1>/*,or_d({prim_multi},or_i(and_v(v:{first_recov_multi},older({csv_values[0]})),and_v(v:{second_recov_multi},older({csv_values[1]})))))"
|
||||
else:
|
||||
return f"wsh(or_d({prim_multi},or_i(and_v(v:{first_recov_multi},older({csv_values[0]})),and_v(v:{second_recov_multi},older({csv_values[1]})))))"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lianad_multipath(bitcoind, directory):
|
||||
datadir = os.path.join(directory, "lianad")
|
||||
@ -211,12 +268,11 @@ def lianad_multipath(bitcoind, directory):
|
||||
|
||||
# A 3-of-4 that degrades into a 3-of-5 after 10 blocks and into a 1-of-10 after 20 blocks.
|
||||
csv_values = [10, 20]
|
||||
signer = MultiSigner(4, {csv_values[0]: 5, csv_values[1]: 10})
|
||||
prim_multi = multi_expression(3, signer.prim_hds)
|
||||
first_recov_multi = multi_expression(3, signer.recov_hds[csv_values[0]])
|
||||
second_recov_multi = multi_expression(1, signer.recov_hds[csv_values[1]])
|
||||
signer = MultiSigner(
|
||||
4, {csv_values[0]: 5, csv_values[1]: 10}, is_taproot=USE_TAPROOT
|
||||
)
|
||||
main_desc = Descriptor.from_str(
|
||||
f"wsh(or_d({prim_multi},or_i(and_v(v:{first_recov_multi},older({csv_values[0]})),and_v(v:{second_recov_multi},older({csv_values[1]})))))"
|
||||
multipath_desc(signer, csv_values, is_taproot=USE_TAPROOT)
|
||||
)
|
||||
|
||||
lianad = Lianad(
|
||||
|
||||
@ -4,4 +4,4 @@ pytest-timeout==1.3.4
|
||||
ephemeral_port_reserve==1.1.1
|
||||
|
||||
bip32~=3.0
|
||||
https://github.com/darosior/python-bip380/archive/f25eb2add9a5d461e382635231a5f971652fc8e1.zip
|
||||
https://github.com/darosior/python-bip380/archive/fb61971d9128e663f110ea2734c1d023e7e0266b.zip
|
||||
|
||||
@ -10,6 +10,7 @@ from test_framework.utils import (
|
||||
COIN,
|
||||
sign_and_broadcast,
|
||||
sign_and_broadcast_psbt,
|
||||
USE_TAPROOT,
|
||||
)
|
||||
from test_framework.serializations import PSBT
|
||||
|
||||
@ -354,6 +355,9 @@ def test_rescan_and_recovery(lianad, bitcoind):
|
||||
sign_and_broadcast(lianad, bitcoind, reco_psbt, recovery=True)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
USE_TAPROOT, reason="Needs a finalizer implemented in the Python test framework."
|
||||
)
|
||||
def test_conflicting_unconfirmed_spend_txs(lianad, bitcoind):
|
||||
"""Test we'll update the spending txid of a coin if a conflicting spend enters our mempool."""
|
||||
# Get an (unconfirmed, on purpose) coin to be spent by 2 different txs.
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from bip32 import BIP32
|
||||
from bip32.utils import coincurve
|
||||
@ -9,10 +10,16 @@ from test_framework.serializations import (
|
||||
PSBT_IN_BIP32_DERIVATION,
|
||||
PSBT_IN_WITNESS_SCRIPT,
|
||||
PSBT_IN_PARTIAL_SIG,
|
||||
PSBT_IN_TAP_KEY_SIG,
|
||||
PSBT_IN_TAP_SCRIPT_SIG,
|
||||
PSBT_IN_TAP_LEAF_SCRIPT,
|
||||
PSBT_IN_TAP_BIP32_DERIVATION,
|
||||
PSBT_IN_TAP_INTERNAL_KEY,
|
||||
PSBT_IN_TAP_MERKLE_ROOT,
|
||||
)
|
||||
|
||||
|
||||
def sign_psbt(psbt, hds):
|
||||
def sign_psbt_wsh(psbt, hds):
|
||||
"""Sign a transaction.
|
||||
|
||||
This will fill the 'partial_sigs' field of all inputs.
|
||||
@ -58,12 +65,47 @@ def sign_psbt(psbt, hds):
|
||||
return psbt
|
||||
|
||||
|
||||
def sign_psbt_taproot(psbt, hds):
|
||||
"""Sign a transaction.
|
||||
|
||||
This will fill the 'tap_script_sig' / 'tap_key_sig' field of all inputs.
|
||||
|
||||
:param psbt: PSBT of the transaction to be signed.
|
||||
:param hds: the BIP32 objects to sign the transaction with.
|
||||
:returns: PSBT with a signature in each input for the given keys.
|
||||
"""
|
||||
assert isinstance(psbt, PSBT)
|
||||
|
||||
# This file is under tests/test_framework/ and we want tests/tools/taproot_signer/target/release/taproot_signer.
|
||||
bin_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"tools",
|
||||
"taproot_signer",
|
||||
"target",
|
||||
"release",
|
||||
"taproot_signer",
|
||||
)
|
||||
if not os.path.exists(bin_path):
|
||||
raise Exception(
|
||||
"Please compile the Taproot signer under tests/tools using 'cargo bin --release'."
|
||||
)
|
||||
|
||||
psbt_str = psbt.to_base64()
|
||||
for hd in hds:
|
||||
xprv = hd.get_xpriv()
|
||||
proc = subprocess.run([bin_path, psbt_str, xprv], capture_output=True, check=True)
|
||||
psbt_str = proc.stdout.decode("utf-8")
|
||||
|
||||
return PSBT.from_base64(psbt_str)
|
||||
|
||||
|
||||
class SingleSigner:
|
||||
"""Assumes a simple 1-primary path 1-recovery path Liana descriptor."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, is_taproot):
|
||||
self.primary_hd = BIP32.from_seed(os.urandom(32), network="test")
|
||||
self.recovery_hd = BIP32.from_seed(os.urandom(32), network="test")
|
||||
self.is_taproot = is_taproot
|
||||
|
||||
def sign_psbt(self, psbt, recovery=False):
|
||||
"""Sign a transaction.
|
||||
@ -75,13 +117,17 @@ class SingleSigner:
|
||||
:returns: PSBT with a signature in each input for the specified key.
|
||||
"""
|
||||
assert isinstance(recovery, bool)
|
||||
return sign_psbt(psbt, [self.recovery_hd if recovery else self.primary_hd])
|
||||
if self.is_taproot:
|
||||
return sign_psbt_taproot(
|
||||
psbt, [self.recovery_hd if recovery else self.primary_hd]
|
||||
)
|
||||
return sign_psbt_wsh(psbt, [self.recovery_hd if recovery else self.primary_hd])
|
||||
|
||||
|
||||
class MultiSigner:
|
||||
"""A signer that has multiple keys and may have multiple recovery path."""
|
||||
|
||||
def __init__(self, primary_hds_count, recovery_hds_counts):
|
||||
def __init__(self, primary_hds_count, recovery_hds_counts, is_taproot):
|
||||
self.prim_hds = [
|
||||
BIP32.from_seed(os.urandom(32), network="test")
|
||||
for _ in range(primary_hds_count)
|
||||
@ -91,6 +137,7 @@ class MultiSigner:
|
||||
self.recov_hds[timelock] = [
|
||||
BIP32.from_seed(os.urandom(32), network="test") for _ in range(count)
|
||||
]
|
||||
self.is_taproot = is_taproot
|
||||
|
||||
def sign_psbt(self, psbt, key_indices):
|
||||
"""Sign a transaction with the keys at the specified indices.
|
||||
@ -106,4 +153,6 @@ class MultiSigner:
|
||||
]
|
||||
else:
|
||||
hds = [self.prim_hds[i] for i in key_indices]
|
||||
return sign_psbt(psbt, hds)
|
||||
if self.is_taproot:
|
||||
return sign_psbt_taproot(psbt, hds)
|
||||
return sign_psbt_wsh(psbt, hds)
|
||||
|
||||
@ -24,6 +24,7 @@ DEFAULT_BITCOIND_PATH = "bitcoind"
|
||||
BITCOIND_PATH = os.getenv("BITCOIND_PATH", DEFAULT_BITCOIND_PATH)
|
||||
OLD_LIANAD_PATH = os.getenv("OLD_LIANAD_PATH", None)
|
||||
IS_NOT_BITCOIND_24 = bool(int(os.getenv("IS_NOT_BITCOIND_24", True)))
|
||||
USE_TAPROOT = bool(int(os.getenv("USE_TAPROOT", False))) # TODO: switch to True in a couple releases.
|
||||
|
||||
|
||||
COIN = 10**8
|
||||
@ -71,6 +72,21 @@ def get_txid(hex_tx):
|
||||
return tx.txid().hex()
|
||||
|
||||
|
||||
def sign_and_broadcast(lianad, bitcoind, psbt, recovery=False):
|
||||
"""Sign a PSBT, finalize it, extract the transaction and broadcast it."""
|
||||
signed_psbt = lianad.signer.sign_psbt(psbt, recovery)
|
||||
# Under Taproot i didn't bother implementing a finalizer in the test suite.
|
||||
if USE_TAPROOT:
|
||||
lianad.rpc.updatespend(signed_psbt.to_base64())
|
||||
txid = signed_psbt.tx.txid().hex()
|
||||
lianad.rpc.broadcastspend(txid)
|
||||
lianad.rpc.delspendtx(txid)
|
||||
return txid
|
||||
finalized_psbt = lianad.finalize_psbt(signed_psbt)
|
||||
tx = finalized_psbt.tx.serialize_with_witness().hex()
|
||||
return bitcoind.rpc.sendrawtransaction(tx)
|
||||
|
||||
|
||||
def spend_coins(lianad, bitcoind, coins):
|
||||
"""Spend these coins, no matter how.
|
||||
This will create a single transaction spending them all at once at the minimum
|
||||
@ -84,21 +100,8 @@ def spend_coins(lianad, bitcoind, coins):
|
||||
bitcoind.rpc.getnewaddress(): total_value - 11 - 31 - 300 * len(coins)
|
||||
}
|
||||
res = lianad.rpc.createspend(destinations, [c["outpoint"] for c in coins], 1)
|
||||
|
||||
signed_psbt = lianad.signer.sign_psbt(PSBT.from_base64(res["psbt"]))
|
||||
finalized_psbt = lianad.finalize_psbt(signed_psbt)
|
||||
tx = finalized_psbt.tx.serialize_with_witness().hex()
|
||||
bitcoind.rpc.sendrawtransaction(tx)
|
||||
|
||||
return tx
|
||||
|
||||
|
||||
def sign_and_broadcast(lianad, bitcoind, psbt, recovery=False):
|
||||
"""Sign a PSBT, finalize it, extract the transaction and broadcast it."""
|
||||
signed_psbt = lianad.signer.sign_psbt(psbt, recovery)
|
||||
finalized_psbt = lianad.finalize_psbt(signed_psbt)
|
||||
tx = finalized_psbt.tx.serialize_with_witness().hex()
|
||||
return bitcoind.rpc.sendrawtransaction(tx)
|
||||
txid = sign_and_broadcast(lianad, bitcoind, PSBT.from_base64(res["psbt"]))
|
||||
return bitcoind.rpc.getrawtransaction(txid)
|
||||
|
||||
|
||||
def sign_and_broadcast_psbt(lianad, psbt):
|
||||
|
||||
@ -14,6 +14,7 @@ from test_framework.utils import (
|
||||
COIN,
|
||||
TIMEOUT,
|
||||
IS_NOT_BITCOIND_24,
|
||||
USE_TAPROOT,
|
||||
)
|
||||
|
||||
from threading import Thread
|
||||
@ -66,9 +67,7 @@ def receive_and_send(lianad, bitcoind):
|
||||
psbt = PSBT.from_base64(res["psbt"])
|
||||
txid = psbt.tx.txid().hex()
|
||||
# If we sign only with two keys it won't be able to finalize
|
||||
with pytest.raises(
|
||||
RpcError, match="Miniscript Error: could not satisfy at index 0"
|
||||
):
|
||||
with pytest.raises(RpcError, match="ould not satisfy.* at index 0"):
|
||||
signed_psbt = lianad.signer.sign_psbt(psbt, range(2))
|
||||
lianad.rpc.updatespend(signed_psbt.to_base64())
|
||||
lianad.rpc.broadcastspend(txid)
|
||||
@ -242,7 +241,7 @@ def test_coinbase_deposit(lianad, bitcoind):
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
OLD_LIANAD_PATH is None, reason="Need the old lianad binary to create the datadir."
|
||||
OLD_LIANAD_PATH is None or USE_TAPROOT, reason="Need the old lianad binary to create the datadir."
|
||||
)
|
||||
def test_migration(lianad_multisig, bitcoind):
|
||||
"""Test we can start a newer lianad on a datadir created by an older lianad."""
|
||||
|
||||
@ -17,6 +17,7 @@ from test_framework.utils import (
|
||||
spend_coins,
|
||||
sign_and_broadcast,
|
||||
sign_and_broadcast_psbt,
|
||||
USE_TAPROOT,
|
||||
)
|
||||
|
||||
|
||||
@ -405,13 +406,21 @@ def test_create_spend(lianad, bitcoind):
|
||||
assert len(spend_psbt.o) == 4
|
||||
assert len(spend_psbt.tx.vout) == 4
|
||||
|
||||
# The transaction must contain the spent transaction for each input.
|
||||
# The transaction must contain the spent transaction for each input for P2WSH. But not for Taproot.
|
||||
# We don't make assumptions about the ordering of PSBT inputs.
|
||||
assert sorted(
|
||||
[psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in spend_psbt.i]
|
||||
) == sorted(
|
||||
[bytes.fromhex(bitcoind.rpc.gettransaction(op[:64])["hex"]) for op in outpoints]
|
||||
)
|
||||
if USE_TAPROOT:
|
||||
assert all(
|
||||
PSBT_IN_NON_WITNESS_UTXO not in psbt_in.map for psbt_in in spend_psbt.i
|
||||
)
|
||||
else:
|
||||
assert sorted(
|
||||
[psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in spend_psbt.i]
|
||||
) == sorted(
|
||||
[
|
||||
bytes.fromhex(bitcoind.rpc.gettransaction(op[:64])["hex"])
|
||||
for op in outpoints
|
||||
]
|
||||
)
|
||||
|
||||
# We can sign it and broadcast it.
|
||||
sign_and_broadcast(lianad, bitcoind, PSBT.from_base64(res["psbt"]))
|
||||
@ -1092,9 +1101,9 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind):
|
||||
rbf_1_res = lianad.rpc.rbfpsbt(first_txid, False, 10)
|
||||
rbf_1_psbt = PSBT.from_base64(rbf_1_res["psbt"])
|
||||
# The inputs are the same in both (no new inputs needed in the replacement).
|
||||
assert sorted(
|
||||
psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in first_psbt.i
|
||||
) == sorted(psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in rbf_1_psbt.i)
|
||||
assert sorted(i.prevout.serialize() for i in first_psbt.tx.vin) == sorted(
|
||||
i.prevout.serialize() for i in rbf_1_psbt.tx.vin
|
||||
)
|
||||
# Check non-change output is the same in both.
|
||||
assert first_psbt.tx.vout[0].nValue == rbf_1_psbt.tx.vout[0].nValue
|
||||
assert first_psbt.tx.vout[0].scriptPubKey == rbf_1_psbt.tx.vout[0].scriptPubKey
|
||||
@ -1114,8 +1123,9 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind):
|
||||
# transaction:
|
||||
with pytest.raises(RpcError, match=f"Feerate too low: 10."):
|
||||
lianad.rpc.rbfpsbt(first_txid, False, 10)
|
||||
# Using 11 for feerate works.
|
||||
lianad.rpc.rbfpsbt(first_txid, False, 11)
|
||||
# Using 11 for feerate works for P2WSH. For Taproot we need 12.
|
||||
feerate = 12 if USE_TAPROOT else 11
|
||||
lianad.rpc.rbfpsbt(first_txid, False, feerate)
|
||||
# Add a new transaction spending the change from the first RBF.
|
||||
desc_1_destinations = {
|
||||
bitcoind.rpc.getnewaddress(): 500_000,
|
||||
@ -1149,12 +1159,12 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind):
|
||||
)
|
||||
)
|
||||
# Now replace the first RBF, which will also remove its descendants.
|
||||
rbf_2_res = lianad.rpc.rbfpsbt(rbf_1_txid, False, 11)
|
||||
rbf_2_res = lianad.rpc.rbfpsbt(rbf_1_txid, False, feerate)
|
||||
rbf_2_psbt = PSBT.from_base64(rbf_2_res["psbt"])
|
||||
# The inputs are the same in both (no new inputs needed in the replacement).
|
||||
assert sorted(
|
||||
psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in rbf_1_psbt.i
|
||||
) == sorted(psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in rbf_2_psbt.i)
|
||||
assert sorted(i.prevout.serialize() for i in rbf_1_psbt.tx.vin) == sorted(
|
||||
i.prevout.serialize() for i in rbf_2_psbt.tx.vin
|
||||
)
|
||||
# Check non-change output is the same in both.
|
||||
assert rbf_1_psbt.tx.vout[0].nValue == rbf_2_psbt.tx.vout[0].nValue
|
||||
assert rbf_1_psbt.tx.vout[0].scriptPubKey == rbf_2_psbt.tx.vout[0].scriptPubKey
|
||||
@ -1204,7 +1214,8 @@ def test_rbfpsbt_insufficient_funds(lianad, bitcoind):
|
||||
spend_txid_1 = sign_and_broadcast_psbt(lianad, spend_psbt_1)
|
||||
|
||||
# We don't have sufficient funds to bump the fee.
|
||||
assert "missing" in lianad.rpc.rbfpsbt(spend_txid_1, False, 2)
|
||||
feerate = 3 if USE_TAPROOT else 2
|
||||
assert "missing" in lianad.rpc.rbfpsbt(spend_txid_1, False, feerate)
|
||||
# We can still cancel it as the coin has enough value to create a single
|
||||
# output at a higher feerate.
|
||||
assert "psbt" in lianad.rpc.rbfpsbt(spend_txid_1, True)
|
||||
@ -1273,8 +1284,8 @@ def test_rbfpsbt_cancel(lianad, bitcoind):
|
||||
# Replacement only has a single input.
|
||||
assert len(rbf_1_psbt.i) == 1
|
||||
# This input is one of the two from the previous transaction.
|
||||
assert rbf_1_psbt.i[0].map[PSBT_IN_NON_WITNESS_UTXO] in [
|
||||
psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in rbf_1_psbt.i
|
||||
assert rbf_1_psbt.tx.vin[0].prevout.serialize() in [
|
||||
i.prevout.serialize() for i in first_psbt.tx.vin
|
||||
]
|
||||
# The replacement only has a change output.
|
||||
assert len(rbf_1_psbt.tx.vout) == 1
|
||||
@ -1340,13 +1351,12 @@ def test_rbfpsbt_cancel(lianad, bitcoind):
|
||||
# Now cancel the first RBF, which will also remove its descendants.
|
||||
rbf_2_res = lianad.rpc.rbfpsbt(rbf_1_txid, True)
|
||||
rbf_2_psbt = PSBT.from_base64(rbf_2_res["psbt"])
|
||||
#
|
||||
assert len(rbf_2_psbt.i) == 1
|
||||
assert (
|
||||
rbf_1_psbt.i[0].map[PSBT_IN_NON_WITNESS_UTXO]
|
||||
== rbf_2_psbt.i[0].map[PSBT_IN_NON_WITNESS_UTXO]
|
||||
)
|
||||
# The inputs are the same in both (no new inputs needed in the replacement).
|
||||
assert len(rbf_2_psbt.tx.vin) == 1
|
||||
assert (
|
||||
rbf_1_psbt.tx.vin[0].prevout.serialize()
|
||||
== rbf_2_psbt.tx.vin[0].prevout.serialize()
|
||||
)
|
||||
|
||||
# Only a single output (change) in the replacement.
|
||||
assert len(rbf_2_psbt.tx.vout) == 1
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from fixtures import *
|
||||
from test_framework.serializations import PSBT, uint256_from_str
|
||||
from test_framework.utils import sign_and_broadcast_psbt, wait_for, COIN, RpcError
|
||||
from test_framework.utils import sign_and_broadcast_psbt, wait_for, COIN, RpcError, USE_TAPROOT
|
||||
|
||||
|
||||
def test_spend_change(lianad, bitcoind):
|
||||
@ -121,11 +121,12 @@ def test_coin_marked_spent(lianad, bitcoind):
|
||||
res = lianad.rpc.createspend(destinations, [outpoint], 1)
|
||||
psbt = PSBT.from_base64(res["psbt"])
|
||||
sign_and_broadcast(psbt)
|
||||
change_amount = 840 if USE_TAPROOT else 830
|
||||
assert len(psbt.o) == 1
|
||||
assert len(res["warnings"]) == 1
|
||||
assert (
|
||||
res["warnings"][0]
|
||||
== "Change amount of 830 sats added to fee as it was too small to create a transaction output."
|
||||
== f"Change amount of {change_amount} sats added to fee as it was too small to create a transaction output."
|
||||
)
|
||||
|
||||
# Spend the third coin to an address of ours, no change
|
||||
@ -137,11 +138,12 @@ def test_coin_marked_spent(lianad, bitcoind):
|
||||
res = lianad.rpc.createspend(destinations, [outpoint_3], 1)
|
||||
psbt = PSBT.from_base64(res["psbt"])
|
||||
sign_and_broadcast(psbt)
|
||||
change_amount = 828 if USE_TAPROOT else 818
|
||||
assert len(psbt.o) == 1
|
||||
assert len(res["warnings"]) == 1
|
||||
assert (
|
||||
res["warnings"][0]
|
||||
== "Change amount of 818 sats added to fee as it was too small to create a transaction output."
|
||||
== f"Change amount of {change_amount} sats added to fee as it was too small to create a transaction output."
|
||||
)
|
||||
|
||||
# Spend the fourth coin to an address of ours, with change
|
||||
@ -223,7 +225,8 @@ def test_send_to_self(lianad, bitcoind):
|
||||
assert len(spend_psbt.o) == len(spend_psbt.tx.vout) == 1
|
||||
|
||||
# Note they may ask for an impossible send-to-self. In this case we'll report missing amount.
|
||||
assert "missing" in lianad.rpc.createspend({}, outpoints, 40500)
|
||||
huge_feerate = 50_000 if USE_TAPROOT else 40_500
|
||||
assert "missing" in lianad.rpc.createspend({}, outpoints, huge_feerate)
|
||||
|
||||
# Sign and broadcast the send-to-self transaction created above.
|
||||
signed_psbt = lianad.signer.sign_psbt(spend_psbt)
|
||||
@ -237,7 +240,12 @@ def test_send_to_self(lianad, bitcoind):
|
||||
# FIXME: a 15% increase is huge.
|
||||
res = bitcoind.rpc.getmempoolentry(spend_txid)
|
||||
spend_feerate = int(res["fees"]["base"] * COIN / res["vsize"])
|
||||
assert specified_feerate <= spend_feerate <= int(specified_feerate * 115 / 100)
|
||||
if not USE_TAPROOT:
|
||||
assert specified_feerate <= spend_feerate <= int(specified_feerate * 115 / 100)
|
||||
else:
|
||||
# FIXME: under Taproot we should not consider the max feerate of all leaves if there
|
||||
# is a spendable internal key.
|
||||
assert specified_feerate <= spend_feerate <= int(specified_feerate * 125 / 100)
|
||||
|
||||
# We should by now only have one coin.
|
||||
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
|
||||
@ -250,7 +258,7 @@ def test_send_to_self(lianad, bitcoind):
|
||||
assert len(lianad.rpc.listaddresses()["addresses"]) == 3
|
||||
# Create a new spend to the receive address with index 3.
|
||||
recv_addr = lianad.rpc.listaddresses(3, 1)["addresses"][0]["receive"]
|
||||
res = lianad.rpc.createspend({recv_addr: 11_955_000}, [], 1)
|
||||
res = lianad.rpc.createspend({recv_addr: 11_955_000}, [], 2)
|
||||
assert "psbt" in res
|
||||
# Max(receive_index, change_index) is now 4:
|
||||
assert len(lianad.rpc.listaddresses()["addresses"]) == 4
|
||||
@ -308,42 +316,48 @@ def test_coin_selection(lianad, bitcoind):
|
||||
dest_addr_2 = bitcoind.rpc.getnewaddress()
|
||||
# If feerate is higher than ancestor, we'll need to pay extra.
|
||||
|
||||
def additional_fees(anc_vsize, anc_fee, target_feerate):
|
||||
"""The additional fee which must have been computed by lianad."""
|
||||
computed_anc_vsize = int(anc_fee / target_feerate)
|
||||
print(f"c ", computed_anc_vsize)
|
||||
extra_vsize = anc_vsize - computed_anc_vsize
|
||||
print("e ", extra_vsize)
|
||||
return extra_vsize * target_feerate
|
||||
|
||||
# Try 10 sat/vb:
|
||||
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 10)
|
||||
feerate = 10
|
||||
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], feerate)
|
||||
assert "psbt" in spend_res_2
|
||||
spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
|
||||
# The spend is using the unconfirmed change.
|
||||
assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
|
||||
bytes.fromhex(spend_txid_1)[::-1]
|
||||
)
|
||||
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"] == 161
|
||||
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN == 339
|
||||
# ancestor vsize at feerate 10 sat/vb = ancestor_fee / 10 = 339 / 10 = 33
|
||||
# extra_weight <= (extra vsize * witness factor) = (161 - 33) * 4 = 512
|
||||
# additional fee at 10 sat/vb (2.5 sat/wu) = 512 * 2.5 = 1280
|
||||
anc_vsize = bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"]
|
||||
anc_fees = int(bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN)
|
||||
additional_fee = additional_fees(anc_vsize, anc_fees, feerate)
|
||||
assert len(spend_res_2["warnings"]) == 1
|
||||
assert (
|
||||
spend_res_2["warnings"][0]
|
||||
== "An additional fee of 1280 sats has been added to pay for ancestors at the target feerate."
|
||||
== f"An additional fee of {additional_fee} sats has been added to pay for ancestors at the target feerate."
|
||||
)
|
||||
|
||||
# Try 3 sat/vb:
|
||||
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 3)
|
||||
feerate = 3
|
||||
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], feerate)
|
||||
assert "psbt" in spend_res_2
|
||||
spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
|
||||
# The spend is using the unconfirmed change.
|
||||
assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
|
||||
bytes.fromhex(spend_txid_1)[::-1]
|
||||
)
|
||||
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"] == 161
|
||||
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN == 339
|
||||
# ancestor vsize at feerate 3 sat/vb = ancestor_fee / 3 = 339 / 3 = 113
|
||||
# extra_weight <= (extra vsize * witness factor) = (161 - 113) * 4 = 192
|
||||
# additional fee at 3 sat/vb (0.75 sat/wu) = 192 * 0.75 = 144
|
||||
anc_vsize = bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"]
|
||||
anc_fees = int(bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN)
|
||||
additional_fee = additional_fees(anc_vsize, anc_fees, feerate)
|
||||
assert len(spend_res_2["warnings"]) == 1
|
||||
assert (
|
||||
spend_res_2["warnings"][0]
|
||||
== "An additional fee of 144 sats has been added to pay for ancestors at the target feerate."
|
||||
== f"An additional fee of {additional_fee} sats has been added to pay for ancestors at the target feerate."
|
||||
)
|
||||
|
||||
# 2 sat/vb is same feerate as ancestor and we have no warnings:
|
||||
@ -378,20 +392,18 @@ def test_coin_selection(lianad, bitcoind):
|
||||
# We'll need to pay extra for each unconfirmed coin's ancestors.
|
||||
outpoints = [c["outpoint"] for c in lianad.rpc.listcoins(["unconfirmed"])["coins"]]
|
||||
|
||||
spend_res_3 = lianad.rpc.createspend({dest_addr_3: 20_000}, outpoints, 10)
|
||||
feerate = 10
|
||||
spend_res_3 = lianad.rpc.createspend({dest_addr_3: 20_000}, outpoints, feerate)
|
||||
assert "psbt" in spend_res_3
|
||||
assert bitcoind.rpc.getmempoolentry(deposit_2)["ancestorsize"] == 165
|
||||
assert bitcoind.rpc.getmempoolentry(deposit_2)["fees"]["ancestor"] * COIN == 165
|
||||
# From above, extra fee for unconfirmed change at 10 sat/vb = 1280.
|
||||
# For unconfirmed non-change:
|
||||
# ancestor vsize at feerate 10 sat/vb = ancestor_fee / 10 = 165 / 10 = 16
|
||||
# extra_weight <= (extra vsize * witness factor) = (165 - 16) * 4 = 596
|
||||
# additional fee at 10 sat/vb (2.5 sat/wu) = 596 * 2.5 = 1490
|
||||
# Sum of extra ancestor fees = 1280 + 1490 = 2770.
|
||||
anc_vsize = bitcoind.rpc.getmempoolentry(deposit_2)["ancestorsize"]
|
||||
anc_fees = int(bitcoind.rpc.getmempoolentry(deposit_2)["fees"]["ancestor"] * COIN)
|
||||
prev_anc_vsize = bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"]
|
||||
prev_anc_fees = int(bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN)
|
||||
additional_fee = additional_fees(anc_vsize, anc_fees, feerate) + additional_fees(prev_anc_vsize, prev_anc_fees, feerate)
|
||||
assert len(spend_res_3["warnings"]) == 1
|
||||
assert (
|
||||
spend_res_3["warnings"][0]
|
||||
== "An additional fee of 2770 sats has been added to pay for ancestors at the target feerate."
|
||||
== f"An additional fee of {additional_fee} sats has been added to pay for ancestors at the target feerate."
|
||||
)
|
||||
spend_psbt_3 = PSBT.from_base64(spend_res_3["psbt"])
|
||||
spend_txid_3 = sign_and_broadcast_psbt(lianad, spend_psbt_3)
|
||||
|
||||
90
tests/tools/taproot_signer/Cargo.lock
generated
Normal file
90
tests/tools/taproot_signer/Cargo.lock
generated
Normal file
@ -0,0 +1,90 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
version = "0.10.0-beta"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bech32",
|
||||
"bitcoin-internals",
|
||||
"bitcoin_hashes",
|
||||
"hex-conservative",
|
||||
"hex_lit",
|
||||
"secp256k1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-internals"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
|
||||
dependencies = [
|
||||
"bitcoin-internals",
|
||||
"hex-conservative",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
|
||||
|
||||
[[package]]
|
||||
name = "hex_lit"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"secp256k1-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "taproot_signer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
]
|
||||
10
tests/tools/taproot_signer/Cargo.toml
Normal file
10
tests/tools/taproot_signer/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "taproot_signer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { version = "0.31", features = ["base64"] }
|
||||
|
||||
# Avoid it being built under Liana's target dir.
|
||||
[workspace]
|
||||
13
tests/tools/taproot_signer/README.md
Normal file
13
tests/tools/taproot_signer/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
A quick and dirty program to sign Taproot PSBTs with a given xpriv. This reuses the hot signer Rust
|
||||
code so i don't have to reimplement everything in Python.
|
||||
|
||||
Usage:
|
||||
```
|
||||
cargo build --release
|
||||
./target/release/taproot_signer "cHNidP8BAFICAAAAASLJVdEybEXc7wUWShMaYhEOjwJL5MYbzg+7zeR44BNGEwAAAAD9////AfRHAAAAAAAAFgAUZb+ZwCt1P282vnthp4cBgN4YNCgAAAAAAAEBKzhKAAAAAAAAIlEggmdBglq+Jt7mG8eFAZJPZ9TNEoqsfUXX+e0tRR4on0pCFcHpCSnxiCBSU6Ddvmwxv9nF+529tCGtEuCM1/zRYlksP2wAEs3HghglLpv2X3mSwNNzlaHY8BKNNas/13WibyuQaSCNnIknSSMfLln9wxqrj+HdoimEWq8EF48QCVphtE8of6wgQC+hw/bp6Z3LosvoFKmVGbi5BhTFxhJ88YlUkHwkyx26IFRPnR3hiTo2F1MW+PzS6hKLjdiQA+aD3DlMZkolBdKAulKcwEIVwekJKfGIIFJToN2+bDG/2cX7nb20Ia0S4IzX/NFiWSw/2HFGAyQNqMHzx0CejOgXB8dPeu9cm47MEKqCRr08ntsmIEkh6Q/Tvcb69Lv4z1KGhY5rH2SVgNb+0L6jjg9jTCA7rQEussAhFkAvocP26emdy6LL6BSplRm4uQYUxcYSfPGJVJB8JMsdOQHYcUYDJA2owfPHQJ6M6BcHx09671ybjswQqoJGvTye29gS2ksSAACAGAAAgB8AAAAAAAAAKgAAACEWSSHpD9O9xvr0u/jPUoaFjmsfZJWA1v7QvqOOD2NMIDtFAWwAEs3HghglLpv2X3mSwNNzlaHY8BKNNas/13WibyuQsLhRtwEAAAACAACAAwAAAAQAAIAFAAAABgAAAAAAAAAqAAAAIRZUT50d4Yk6NhdTFvj80uoSi43YkAPmg9w5TGZKJQXSgDUB2HFGAyQNqMHzx0CejOgXB8dPeu9cm47MEKqCRr08ntuwuFG3EgAAgBkAAIAAAAAAKgAAACEWjZyJJ0kjHy5Z/cMaq4/h3aIphFqvBBePEAlaYbRPKH9BAdhxRgMkDajB88dAnozoFwfHT3rvXJuOzBCqgka9PJ7bJxAoeAAAAIAMAACAKgAAAKQBAAA4AAAAAAAAACoAAAAhFukJKfGIIFJToN2+bDG/2cX7nb20Ia0S4IzX/NFiWSw/DQB8Rh5dAAAAACoAAAABFyDpCSnxiCBSU6Ddvmwxv9nF+529tCGtEuCM1/zRYlksPwEYIKKzzcTwLrwB4DXcLXJYeZVMlpkb1zTIpFzsTWdZJU7nAAA=" xprv9s21ZrQH143K3fRVbj9JuCc3Eaph4F7AKXUtLGs9AWxWmhyMGq5ur5NqH3fDuiQZ6GamXbW7L3msaCwRbstxVqc3eP2FX3oa9wABxVzHr1k
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
cHNidP8BAFICAAAAASLJVdEybEXc7wUWShMaYhEOjwJL5MYbzg+7zeR44BNGEwAAAAD9////AfRHAAAAAAAAFgAUZb+ZwCt1P282vnthp4cBgN4YNCgAAAAAAAEBKzhKAAAAAAAAIlEggmdBglq+Jt7mG8eFAZJPZ9TNEoqsfUXX+e0tRR4on0pBFI2ciSdJIx8uWf3DGquP4d2iKYRarwQXjxAJWmG0Tyh/2HFGAyQNqMHzx0CejOgXB8dPeu9cm47MEKqCRr08nttAJ148PhMM/k/GnKCQ09RkpEKYCAHwVh3UMueWbq0TcIMW8s3XKNk1vyFy3LrXdnGghS8Up/i4bdNR2ikX1XdlYEIVwekJKfGIIFJToN2+bDG/2cX7nb20Ia0S4IzX/NFiWSw/bAASzceCGCUum/ZfeZLA03OVodjwEo01qz/XdaJvK5BpII2ciSdJIx8uWf3DGquP4d2iKYRarwQXjxAJWmG0Tyh/rCBAL6HD9unpncuiy+gUqZUZuLkGFMXGEnzxiVSQfCTLHbogVE+dHeGJOjYXUxb4/NLqEouN2JAD5oPcOUxmSiUF0oC6UpzAQhXB6Qkp8YggUlOg3b5sMb/ZxfudvbQhrRLgjNf80WJZLD/YcUYDJA2owfPHQJ6M6BcHx09671ybjswQqoJGvTye2yYgSSHpD9O9xvr0u/jPUoaFjmsfZJWA1v7QvqOOD2NMIDutAS6ywCEWQC+hw/bp6Z3LosvoFKmVGbi5BhTFxhJ88YlUkHwkyx05AdhxRgMkDajB88dAnozoFwfHT3rvXJuOzBCqgka9PJ7b2BLaSxIAAIAYAACAHwAAAAAAAAAqAAAAIRZJIekP073G+vS7+M9ShoWOax9klYDW/tC+o44PY0wgO0UBbAASzceCGCUum/ZfeZLA03OVodjwEo01qz/XdaJvK5CwuFG3AQAAAAIAAIADAAAABAAAgAUAAAAGAAAAAAAAACoAAAAhFlRPnR3hiTo2F1MW+PzS6hKLjdiQA+aD3DlMZkolBdKANQHYcUYDJA2owfPHQJ6M6BcHx09671ybjswQqoJGvTye27C4UbcSAACAGQAAgAAAAAAqAAAAIRaNnIknSSMfLln9wxqrj+HdoimEWq8EF48QCVphtE8of0EB2HFGAyQNqMHzx0CejOgXB8dPeu9cm47MEKqCRr08ntsnECh4AAAAgAwAAIAqAAAApAEAADgAAAAAAAAAKgAAACEW6Qkp8YggUlOg3b5sMb/ZxfudvbQhrRLgjNf80WJZLD8NAHxGHl0AAAAAKgAAAAEXIOkJKfGIIFJToN2+bDG/2cX7nb20Ia0S4IzX/NFiWSw/ARggorPNxPAuvAHgNdwtclh5lUyWmRvXNMikXOxNZ1klTucAAA==
|
||||
```
|
||||
119
tests/tools/taproot_signer/src/main.rs
Normal file
119
tests/tools/taproot_signer/src/main.rs
Normal file
@ -0,0 +1,119 @@
|
||||
//! A quick and dirty program which reads a PSBT and an xpriv from stdin and outputs the signed
|
||||
//! PSBT to stdout. Uses function copied from Liana's hot signer and adapted.
|
||||
|
||||
use std::{
|
||||
env,
|
||||
io::{self, Write},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bitcoin::{
|
||||
self,
|
||||
bip32::{self, Xpriv},
|
||||
hashes::Hash,
|
||||
key::TapTweak,
|
||||
psbt::{Input as PsbtIn, Psbt},
|
||||
secp256k1, sighash,
|
||||
};
|
||||
|
||||
fn sign_taproot(
|
||||
secp: &secp256k1::Secp256k1<secp256k1::All>,
|
||||
sighash_cache: &mut sighash::SighashCache<&bitcoin::Transaction>,
|
||||
master_xpriv: Xpriv,
|
||||
master_fingerprint: bip32::Fingerprint,
|
||||
prevouts: &[bitcoin::TxOut],
|
||||
psbt_in: &mut PsbtIn,
|
||||
input_index: usize,
|
||||
) {
|
||||
let sig_type = sighash::TapSighashType::Default;
|
||||
let prevouts = sighash::Prevouts::All(prevouts);
|
||||
|
||||
// If the details of the internal key are filled, provide a keypath signature.
|
||||
if let Some(ref int_key) = psbt_in.tap_internal_key {
|
||||
// NB: we don't check for empty leaf hashes on purpose, in case the internal key also
|
||||
// appears in a leaf.
|
||||
if let Some((_, (fg, der_path))) = psbt_in.tap_key_origins.get(int_key) {
|
||||
if *fg == master_fingerprint {
|
||||
let privkey = master_xpriv.derive_priv(secp, der_path).unwrap().to_priv();
|
||||
let keypair = secp256k1::Keypair::from_secret_key(secp, &privkey.inner);
|
||||
assert_eq!(keypair.x_only_public_key().0, *int_key);
|
||||
let keypair = keypair.tap_tweak(secp, psbt_in.tap_merkle_root).to_inner();
|
||||
let sighash = sighash_cache
|
||||
.taproot_key_spend_signature_hash(input_index, &prevouts, sig_type)
|
||||
.unwrap();
|
||||
let sighash = secp256k1::Message::from_digest_slice(sighash.as_byte_array())
|
||||
.expect("Sighash is always 32 bytes.");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&sighash, &keypair);
|
||||
let sig = bitcoin::taproot::Signature {
|
||||
sig,
|
||||
hash_ty: sig_type,
|
||||
};
|
||||
psbt_in.tap_key_sig = Some(sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now sign for all the public keys derived from our master secret, in all the leaves where
|
||||
// they are present.
|
||||
for (pubkey, (leaf_hashes, (fg, der_path))) in &psbt_in.tap_key_origins {
|
||||
if *fg != master_fingerprint {
|
||||
continue;
|
||||
}
|
||||
|
||||
for leaf_hash in leaf_hashes {
|
||||
let privkey = master_xpriv.derive_priv(secp, der_path).unwrap().to_priv();
|
||||
let keypair = secp256k1::Keypair::from_secret_key(secp, &privkey.inner);
|
||||
let sighash = sighash_cache
|
||||
.taproot_script_spend_signature_hash(input_index, &prevouts, *leaf_hash, sig_type)
|
||||
.unwrap();
|
||||
let sighash = secp256k1::Message::from_digest_slice(sighash.as_byte_array())
|
||||
.expect("Sighash is always 32 bytes.");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&sighash, &keypair);
|
||||
let sig = bitcoin::taproot::Signature {
|
||||
sig,
|
||||
hash_ty: sig_type,
|
||||
};
|
||||
psbt_in.tap_script_sigs.insert((*pubkey, *leaf_hash), sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_psbt(psbt: &mut Psbt, master_xpriv: Xpriv, secp: &secp256k1::Secp256k1<secp256k1::All>) {
|
||||
let mut sighash_cache = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||
let master_fingerprint = master_xpriv.fingerprint(secp);
|
||||
|
||||
let prevouts: Vec<_> = psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|psbt_in| psbt_in.witness_utxo.clone())
|
||||
.collect();
|
||||
assert_eq!(prevouts.len(), psbt.inputs.len());
|
||||
|
||||
// Sign each input in the PSBT.
|
||||
for i in 0..psbt.inputs.len() {
|
||||
sign_taproot(
|
||||
secp,
|
||||
&mut sighash_cache,
|
||||
master_xpriv,
|
||||
master_fingerprint,
|
||||
&prevouts,
|
||||
&mut psbt.inputs[i],
|
||||
i,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
assert_eq!(args.len(), 3);
|
||||
|
||||
let mut psbt = Psbt::from_str(&args[1]).unwrap();
|
||||
let xprv = Xpriv::from_str(&args[2]).unwrap();
|
||||
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
sign_psbt(&mut psbt, xprv, &secp);
|
||||
|
||||
let psbt_str = psbt.to_string();
|
||||
print!("{}", psbt_str);
|
||||
io::stdout().flush().unwrap();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user