Skip to content

Commit 962bcc2

Browse files
evanlinjinclaude
andcommitted
plan: Append witness script for P2WSH in Plan::satisfy()
Fix Plan::satisfy() to correctly append the witnessScript as the final witness stack element for P2WSH descriptor types (Wsh, WshSortedMulti, ShWsh, ShWshSortedMulti). Previously, these types were incorrectly grouped with Wpkh and Tr, which don't require a trailing witness script. This caused transactions built using Plan::satisfy() to fail validation with "Witness program hash mismatch" when broadcast. The fix separates the descriptor type handling: - Wpkh/Tr: return stack as-is (no witness script needed) - Wsh/WshSortedMulti: append witness script, empty script_sig - ShWpkh: return stack with unsigned_script_sig (no witness script) - ShWsh/ShWshSortedMulti: append witness script and unsigned_script_sig Closes #896 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 08f7b62 commit 962bcc2

File tree

1 file changed

+128
-5
lines changed

1 file changed

+128
-5
lines changed

src/plan.rs

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,24 @@ impl Plan {
292292
})
293293
.into_script(),
294294
),
295-
DescriptorType::Wpkh
296-
| DescriptorType::Wsh
297-
| DescriptorType::WshSortedMulti
298-
| DescriptorType::Tr => (stack, ScriptBuf::new()),
299-
DescriptorType::ShWsh | DescriptorType::ShWshSortedMulti | DescriptorType::ShWpkh => {
295+
DescriptorType::Wpkh | DescriptorType::Tr => (stack, ScriptBuf::new()),
296+
DescriptorType::Wsh | DescriptorType::WshSortedMulti => {
297+
let mut stack = stack;
298+
let witness_script = self
299+
.descriptor
300+
.explicit_script()
301+
.expect("descriptor is wsh");
302+
stack.push(witness_script.into_bytes());
303+
(stack, ScriptBuf::new())
304+
}
305+
DescriptorType::ShWpkh => (stack, self.descriptor.unsigned_script_sig()),
306+
DescriptorType::ShWsh | DescriptorType::ShWshSortedMulti => {
307+
let mut stack = stack;
308+
let witness_script = self
309+
.descriptor
310+
.explicit_script()
311+
.expect("descriptor is sh-wsh");
312+
stack.push(witness_script.into_bytes());
300313
(stack, self.descriptor.unsigned_script_sig())
301314
}
302315
})
@@ -1154,4 +1167,114 @@ mod test {
11541167
assert!(psbt_input.redeem_script.is_none(), "Redeem script present");
11551168
assert_eq!(psbt_input.bip32_derivation.len(), 2, "Unexpected number of bip32_derivation");
11561169
}
1170+
1171+
#[test]
1172+
fn test_plan_satisfy_wsh() {
1173+
use std::collections::BTreeMap;
1174+
1175+
use bitcoin::secp256k1::{self, Secp256k1};
1176+
1177+
let secp = Secp256k1::new();
1178+
// Generate a key from deterministic secret key
1179+
let sk =
1180+
secp256k1::SecretKey::from_slice(&b"sally was a secret key, she said"[..]).unwrap();
1181+
let pk = bitcoin::PublicKey::new(secp256k1::PublicKey::from_secret_key(&secp, &sk));
1182+
let msg = secp256k1::Message::from_digest_slice(&b"michael was a message, amusingly"[..])
1183+
.expect("32 bytes");
1184+
let sig = secp.sign_ecdsa(&msg, &sk);
1185+
let ecdsa_sig = bitcoin::ecdsa::Signature {
1186+
signature: sig,
1187+
sighash_type: bitcoin::sighash::EcdsaSighashType::All,
1188+
};
1189+
1190+
// Create a wsh(pk(key)) descriptor - simple single key
1191+
let desc_str = format!("wsh(pk({}))", pk);
1192+
let desc = Descriptor::<DefiniteDescriptorKey>::from_str(&desc_str).unwrap();
1193+
1194+
// Get the witness script for comparison
1195+
let witness_script = desc.explicit_script().expect("wsh has explicit script");
1196+
1197+
// Create the DefiniteDescriptorKey for the satisfier and DescriptorPublicKey for assets
1198+
let pk_str = pk.to_string();
1199+
let definite_key = DefiniteDescriptorKey::from_str(&pk_str).unwrap();
1200+
let descriptor_key = DescriptorPublicKey::from_str(&pk_str).unwrap();
1201+
1202+
// Create a BTreeMap satisfier with the signature
1203+
let mut sig_map: BTreeMap<DefiniteDescriptorKey, bitcoin::ecdsa::Signature> =
1204+
BTreeMap::new();
1205+
sig_map.insert(definite_key, ecdsa_sig);
1206+
1207+
// Create assets and get a plan
1208+
let assets = Assets::new().add(descriptor_key);
1209+
let plan = desc.plan(&assets).expect("plan should succeed");
1210+
1211+
// Call plan.satisfy() with the signature satisfier
1212+
let (witness, script_sig) = plan.satisfy(&sig_map).expect("satisfy should succeed");
1213+
1214+
// For native P2WSH:
1215+
// - script_sig should be empty
1216+
// - witness should contain [signature, witness_script]
1217+
assert!(script_sig.is_empty(), "P2WSH script_sig should be empty");
1218+
assert_eq!(witness.len(), 2, "P2WSH witness should have 2 elements (sig + witness_script)");
1219+
assert_eq!(
1220+
witness.last().unwrap(),
1221+
&witness_script.into_bytes(),
1222+
"Last witness element should be the witness script"
1223+
);
1224+
}
1225+
1226+
#[test]
1227+
fn test_plan_satisfy_sh_wsh() {
1228+
use std::collections::BTreeMap;
1229+
1230+
use bitcoin::secp256k1::{self, Secp256k1};
1231+
1232+
let secp = Secp256k1::new();
1233+
let sk =
1234+
secp256k1::SecretKey::from_slice(&b"sally was a secret key, she said"[..]).unwrap();
1235+
let pk = bitcoin::PublicKey::new(secp256k1::PublicKey::from_secret_key(&secp, &sk));
1236+
let msg = secp256k1::Message::from_digest_slice(&b"michael was a message, amusingly"[..])
1237+
.expect("32 bytes");
1238+
let sig = secp.sign_ecdsa(&msg, &sk);
1239+
let ecdsa_sig = bitcoin::ecdsa::Signature {
1240+
signature: sig,
1241+
sighash_type: bitcoin::sighash::EcdsaSighashType::All,
1242+
};
1243+
1244+
// Create a sh(wsh(pk(key))) descriptor
1245+
let desc_str = format!("sh(wsh(pk({})))", pk);
1246+
let desc = Descriptor::<DefiniteDescriptorKey>::from_str(&desc_str).unwrap();
1247+
1248+
// Get the witness script (inner script of the wsh)
1249+
let witness_script = desc.explicit_script().expect("sh-wsh has explicit script");
1250+
1251+
// Create the DefiniteDescriptorKey for the satisfier and DescriptorPublicKey for assets
1252+
let pk_str = pk.to_string();
1253+
let definite_key = DefiniteDescriptorKey::from_str(&pk_str).unwrap();
1254+
let descriptor_key = DescriptorPublicKey::from_str(&pk_str).unwrap();
1255+
1256+
let mut sig_map: BTreeMap<DefiniteDescriptorKey, bitcoin::ecdsa::Signature> =
1257+
BTreeMap::new();
1258+
sig_map.insert(definite_key, ecdsa_sig);
1259+
1260+
let assets = Assets::new().add(descriptor_key);
1261+
let plan = desc.plan(&assets).expect("plan should succeed");
1262+
1263+
let (witness, script_sig) = plan.satisfy(&sig_map).expect("satisfy should succeed");
1264+
1265+
// For P2SH-P2WSH:
1266+
// - script_sig should contain the P2WSH script (witness script hash)
1267+
// - witness should contain [signature, witness_script]
1268+
assert!(!script_sig.is_empty(), "P2SH-P2WSH script_sig should not be empty");
1269+
assert_eq!(
1270+
witness.len(),
1271+
2,
1272+
"P2SH-P2WSH witness should have 2 elements (sig + witness_script)"
1273+
);
1274+
assert_eq!(
1275+
witness.last().unwrap(),
1276+
&witness_script.into_bytes(),
1277+
"Last witness element should be the witness script"
1278+
);
1279+
}
11571280
}

0 commit comments

Comments
 (0)