Skip to content

Commit bb72945

Browse files
authored
Merge pull request #93 from Foundation-Devices/SFT-6018
SFT-6018: Add P2WSH wrapped in P2SH.
2 parents 062f589 + a5676cf commit bb72945

File tree

2 files changed

+159
-24
lines changed

2 files changed

+159
-24
lines changed

src/psbt.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ pub enum Error {
227227
/// The script of the output is unknown and can't be validated or shown
228228
/// to the user if not a change address.
229229
#[error("the output number {index} script type is unknown")]
230-
UnknownScript { index: usize },
230+
UnknownOutputScript { index: usize },
231231

232232
/// The PSBT specifies multiple keys for an output that belongs to us
233233
/// but the script type for the output is single-sig only, e.g. P2PKH,
@@ -514,7 +514,7 @@ where
514514
return Err(Error::Unimplemented);
515515
}
516516
} else {
517-
return Err(Error::InvalidWitnessScript { index: i });
517+
return Err(Error::MissingWitnessScript { index: i });
518518
}
519519
} else if funding_utxo.script_pubkey.is_p2sh() {
520520
if let Some(redeem_script) = input.redeem_script.as_ref() {
@@ -538,8 +538,24 @@ where
538538
descriptors.insert(p2sh::p2shwpkh_descriptor(
539539
secp, master_key, &source.1, network,
540540
));
541+
} else if redeem_script.is_p2wsh() {
542+
if let Some(witness_script) = input.witness_script.as_ref() {
543+
if witness_script.is_multisig() {
544+
let required_signers = multisig::disassemble(witness_script).unwrap();
545+
descriptors.insert(p2sh::wsh_multisig_descriptor(
546+
required_signers,
547+
&psbt.xpub,
548+
&input.bip32_derivation,
549+
)?);
550+
} else {
551+
return Err(Error::Unimplemented);
552+
}
553+
} else {
554+
return Err(Error::MissingWitnessScript { index: i });
555+
}
541556
} else {
542-
return Err(Error::Unimplemented);
557+
// TODO: Change to UnknownInputScript
558+
return Err(Error::UnknownOutputScript { index: i });
543559
}
544560
} else {
545561
return Err(Error::InvalidRedeemScript { index: i });
@@ -768,7 +784,7 @@ where
768784
op_return::parse(txout)
769785
} else {
770786
let address = Address::from_script(&txout.script_pubkey, network.params())
771-
.map_err(|_| Error::UnknownScript { index })?;
787+
.map_err(|_| Error::UnknownOutputScript { index })?;
772788

773789
PsbtOutput {
774790
amount: txout.value,
@@ -794,7 +810,7 @@ where
794810
// this output type.
795811
Err(Error::DeprecatedOutputType { index })
796812
} else {
797-
Err(Error::UnknownScript { index })
813+
Err(Error::UnknownOutputScript { index })
798814
}
799815
}
800816

src/psbt/p2sh.rs

Lines changed: 138 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ use crate::bip32::NgAccountPath;
22
use crate::psbt::{
33
Error, OutputKind, PsbtOutput, derive_account_xpub, derive_full_descriptor_pubkey,
44
};
5-
use bdk_wallet::bitcoin::bip32::{ChildNumber, Xpriv};
5+
use bdk_wallet::bitcoin::bip32::{ChildNumber, DerivationPath, KeySource, Xpriv, Xpub};
66
use bdk_wallet::bitcoin::psbt;
7-
use bdk_wallet::bitcoin::secp256k1::{Secp256k1, Signing};
7+
use bdk_wallet::bitcoin::secp256k1::{PublicKey, Secp256k1, Signing};
88
use bdk_wallet::bitcoin::{Address, CompressedPublicKey, Network, TxOut};
9-
use bdk_wallet::descriptor::ExtendedDescriptor;
10-
use bdk_wallet::miniscript::descriptor::Wpkh;
9+
use bdk_wallet::descriptor::{Descriptor, ExtendedDescriptor, Segwitv0};
10+
use bdk_wallet::keys::DescriptorPublicKey;
11+
use bdk_wallet::miniscript::descriptor::{DerivPaths, DescriptorMultiXKey, Wildcard};
12+
use bdk_wallet::miniscript::descriptor::{Sh, Wpkh};
13+
use bdk_wallet::miniscript::{ForEachKey, Miniscript};
1114
use bdk_wallet::template::{Bip49Public, DescriptorTemplate};
15+
use std::collections::BTreeMap;
1216

1317
pub fn validate_output(
1418
output: &psbt::Output,
@@ -18,26 +22,95 @@ pub fn validate_output(
1822
) -> Result<PsbtOutput, Error> {
1923
debug_assert!(txout.script_pubkey.is_p2sh());
2024

21-
// There should be at least one.
22-
if output.bip32_derivation.is_empty() {
23-
return Err(Error::ExpectedKeys { index });
24-
}
25+
let redeem_script = output
26+
.redeem_script
27+
.as_ref()
28+
.ok_or_else(|| Error::MissingRedeemScript { index })?;
2529

26-
let (_, source) = output
27-
.bip32_derivation
28-
.first_key_value()
29-
.expect("the previous statement checks for at least one entry");
30+
if redeem_script.is_p2wpkh() {
31+
validate_p2wpkh_nested_in_p2sh_output(output, txout, network, index)
32+
} else if redeem_script.is_p2wsh() {
33+
let witness_script = output
34+
.witness_script
35+
.as_ref()
36+
.ok_or_else(|| Error::MissingWitnessScript { index })?;
37+
38+
let ms = Miniscript::<_, Segwitv0>::parse(witness_script).unwrap();
39+
let descriptor = Sh::new_wsh(ms).map(Descriptor::Sh).unwrap();
40+
41+
// Verify that all keys in the descriptor are in the bip32_derivation map
42+
// which should have been validated already.
43+
let are_keys_valid =
44+
descriptor.for_each_key(|pk| output.bip32_derivation.contains_key(&pk.inner));
45+
if !are_keys_valid {
46+
return Err(Error::FraudulentOutput { index });
47+
}
48+
49+
let address = descriptor.address(network).unwrap();
50+
if !address.matches_script_pubkey(&txout.script_pubkey) {
51+
return Err(Error::FraudulentOutput { index });
52+
}
53+
54+
let (_, (_, path)) = output
55+
.bip32_derivation
56+
.first_key_value()
57+
.expect("at least one bip32 derivation should be present");
58+
59+
let Some(purpose) = path.as_ref().iter().next() else {
60+
return Ok(PsbtOutput {
61+
amount: txout.value,
62+
kind: OutputKind::Suspicious(address),
63+
});
64+
};
65+
66+
// TODO: Add support for other BIPs here.
67+
if matches!(purpose, ChildNumber::Hardened { index: 48 }) {
68+
// For BIP-0048 all paths used to derive an address should be equal.
69+
let mut are_paths_equal = true;
70+
for (_, (_, other_path)) in output.bip32_derivation.iter() {
71+
if other_path != path {
72+
are_paths_equal = false;
73+
break;
74+
}
75+
}
76+
77+
if !are_paths_equal {
78+
return Ok(PsbtOutput {
79+
amount: txout.value,
80+
kind: OutputKind::Suspicious(address),
81+
});
82+
}
3083

31-
if let Some(purpose) = source.1.as_ref().iter().next() {
32-
match purpose {
33-
ChildNumber::Hardened { index: 49 } => {
34-
return validate_p2wpkh_nested_in_p2sh_output(output, txout, network, index);
84+
let maybe_account_path =
85+
NgAccountPath::parse(path).map_err(|e| Error::invalid_path(path.clone(), e))?;
86+
let Some(account_path) = maybe_account_path else {
87+
return Ok(PsbtOutput {
88+
amount: txout.value,
89+
kind: OutputKind::Suspicious(address),
90+
});
91+
};
92+
93+
if !matches!(account_path.script_type, Some(1)) {
94+
return Ok(PsbtOutput {
95+
amount: txout.value,
96+
kind: OutputKind::Suspicious(address),
97+
});
3598
}
36-
_ => return Err(Error::Unimplemented),
99+
100+
Ok(PsbtOutput {
101+
amount: txout.value,
102+
kind: OutputKind::from_derivation_path(path, 48, network, address)?,
103+
})
104+
} else {
105+
Ok(PsbtOutput {
106+
amount: txout.value,
107+
kind: OutputKind::Suspicious(address),
108+
})
37109
}
110+
} else {
111+
// TODO: Legacy P2SH (e.g. BIP45).
112+
Err(Error::Unimplemented)
38113
}
39-
40-
Err(Error::Unimplemented)
41114
}
42115

43116
fn validate_p2wpkh_nested_in_p2sh_output(
@@ -105,3 +178,49 @@ where
105178
}
106179
}
107180
}
181+
182+
/// Returns the descriptor for a P2WSH wrapped in P2SH multisig account.
183+
///
184+
/// The `required_signers` parameter must be known before hand, by for
185+
/// example, disassembling the multisig script.
186+
pub fn wsh_multisig_descriptor(
187+
required_signers: u8,
188+
global_xpubs: &BTreeMap<Xpub, KeySource>,
189+
bip32_derivations: &BTreeMap<PublicKey, KeySource>,
190+
) -> Result<ExtendedDescriptor, Error> {
191+
// Find the account Xpubs in the global Xpub map of the PSBT.
192+
let xpubs = bip32_derivations
193+
.iter()
194+
.map(|(_, (subpath_fingerprint, subpath))| {
195+
global_xpubs
196+
.iter()
197+
.find(|(_, (global_fingerprint, global_path))| {
198+
subpath_fingerprint == global_fingerprint
199+
&& subpath.as_ref().starts_with(global_path.as_ref())
200+
})
201+
.ok_or_else(|| Error::MissingGlobalXpub(subpath.clone()))
202+
});
203+
204+
let mut descriptor_pubkeys = Vec::new();
205+
for maybe_xpub in xpubs {
206+
let (xpub, source) = maybe_xpub?;
207+
208+
let descriptor_pubkey = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
209+
origin: Some(source.clone()),
210+
xkey: *xpub,
211+
derivation_paths: DerivPaths::new(vec![
212+
DerivationPath::from(vec![ChildNumber::Normal { index: 0 }]),
213+
DerivationPath::from(vec![ChildNumber::Normal { index: 1 }]),
214+
])
215+
.expect("the vector passed should not be empty"),
216+
wildcard: Wildcard::Unhardened,
217+
});
218+
descriptor_pubkeys.push(descriptor_pubkey);
219+
}
220+
221+
Ok(ExtendedDescriptor::new_sh_wsh_sortedmulti(
222+
usize::from(required_signers),
223+
descriptor_pubkeys,
224+
)
225+
.unwrap())
226+
}

0 commit comments

Comments
 (0)