Skip to content

Commit 874db20

Browse files
author
+Sharon
committed
Add DecodeScriptSegwit struct and support in DecodeScript conversion
- Add `DecodeScriptSegwit` struct to model the `segwit` field returned by the `decodescript` RPC. - Update `DecodeScript` to include an optional `segwit` field. - Add `raw_transactions`folder to v19. Add DecodeScriptSegwit struct, conversions, and model support - Add `DecodeScriptSegwit` struct to both versioned and model representations. - Implement `into_model()` for `DecodeScriptSegwit` and update `DecodeScript` accordingly. - Use `ScriptBuf` instead of `String` for `hex` to strongly type the field. - Replace `String` with `Address<NetworkUnchecked>` for `p2sh_segwit` and other fields. - Normalize and correct field comments to match Core `decodescript` RPC output. - Clean up formatting errors Add DecodeScriptSegwit into_model to v17 and refactor error handling - Add `into_model` implementation for `DecodeScriptSegwit` in v17. - Return `segwit` in v17, as it is present in RPC output despite not being documented until v19. - Add `DecodeScriptSegwitError` enum in v17, as `address` is sometimes `None` and error handling is needed. - Remove duplicate `DecodeScriptSegwitError` from v23 and reuse the one from v22 via import. - Move `descriptor` field in `DecodeScriptSegwit` model struct to match the field order in Bitcoin Core's `decodescript` RPC response. Add model test for decode_script with P2WPKH SegWit output Add model test for decode_script_segwit inyo model
1 parent b779398 commit 874db20

File tree

18 files changed

+612
-63
lines changed

18 files changed

+612
-63
lines changed

integration_test/tests/raw_transactions.rs

Lines changed: 171 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44
55
#![allow(non_snake_case)] // Test names intentionally use double underscore.
66
#![allow(unused_imports)] // Because of feature gated tests.
7-
87
use bitcoin::consensus::encode;
98
use bitcoin::hex::FromHex as _;
109
use bitcoin::opcodes::all::*;
1110
use bitcoin::{
1211
absolute, consensus, hex, psbt, script, transaction, Amount, ScriptBuf, Transaction, TxOut,
12+
Address, Network, hashes::{hash160,sha256,Hash}, WPubkeyHash, WScriptHash, secp256k1, PublicKey,
13+
script::Builder, key::{Secp256k1, XOnlyPublicKey}, address::NetworkUnchecked,
1314
};
1415
use integration_test::{Node, NodeExt as _, Wallet};
1516
use node::vtype::*;
1617
use node::{mtype, Input, Output}; // All the version specific types.
18+
use rand::Rng;
19+
1720

1821
#[test]
1922
#[cfg(not(feature = "v17"))] // analyzepsbt was added in v0.18.
@@ -201,18 +204,63 @@ fn raw_transactions__decode_script__modelled() {
201204
let node = Node::with_wallet(Wallet::Default, &["-txindex"]);
202205
node.fund_wallet();
203206

204-
let p2pkh = arbitrary_p2pkh_script();
205-
let multi = arbitrary_multisig_script();
206-
207-
for script in &[p2pkh, multi] {
207+
let test_cases: Vec<(&str, ScriptBuf, Option<&str>)> = vec![
208+
("p2pkh", arbitrary_p2pkh_script(), Some("pubkeyhash")),
209+
("multisig", arbitrary_multisig_script(), Some("multisig")),
210+
("p2sh", arbitrary_p2sh_script(), Some("scripthash")),
211+
("bare", arbitrary_bare_script(), Some("nonstandard")),
212+
("p2wpkh", arbitrary_p2wpkh_script(), Some("witness_v0_keyhash")),
213+
("p2wsh", arbitrary_p2wsh_script(), Some("witness_v0_scripthash")),
214+
("p2tr", arbitrary_p2tr_script(), Some("witness_v1_taproot")),
215+
];
216+
217+
for (label, script, expected_type) in test_cases {
208218
let hex = script.to_hex_string();
209219

210220
let json: DecodeScript = node.client.decode_script(&hex).expect("decodescript");
211221
let model: Result<mtype::DecodeScript, DecodeScriptError> = json.into_model();
212-
model.unwrap();
222+
let decoded = model.expect("DecodeScript into model");
223+
224+
println!("Decoded script ({label}): {:?}", decoded);
225+
226+
if let Some(expected) = expected_type {
227+
assert_eq!(decoded.type_, expected, "Unexpected script type for {label}");
228+
} else {
229+
println!("Skipping type check for {}", label);
230+
}
231+
232+
// Address should be present for standard scripts
233+
if expected_type != Some("nonstandard") {
234+
let has_any_address = !decoded.addresses.is_empty() || decoded.address.is_some();
235+
assert!(
236+
has_any_address,
237+
"Expected at least one address for {label}"
238+
);
239+
}
213240
}
214241
}
242+
fn arbitrary_p2sh_script() -> ScriptBuf {
243+
244+
let redeem_script = arbitrary_multisig_script(); // or arbitrary_p2pkh_script()
245+
let redeem_script_hash = hash160::Hash::hash(redeem_script.as_bytes());
215246

247+
script::Builder::new()
248+
.push_opcode(bitcoin::opcodes::all::OP_HASH160)
249+
.push_slice(redeem_script_hash.as_byte_array()) // [u8; 20]
250+
.push_opcode(bitcoin::opcodes::all::OP_EQUAL)
251+
.into_script()
252+
}
253+
fn arbitrary_bare_script() -> ScriptBuf {
254+
script::Builder::new()
255+
.push_opcode(OP_RETURN)
256+
.push_slice(b"hello")
257+
.into_script()
258+
}
259+
fn arbitrary_pubkey() -> PublicKey {
260+
let secp = Secp256k1::new();
261+
let secret_key = secp256k1::SecretKey::from_slice(&[1u8; 32]).unwrap();
262+
PublicKey::new(secp256k1::PublicKey::from_secret_key(&secp, &secret_key))
263+
}
216264
// Script builder code copied from rust-bitcoin script unit tests.
217265
fn arbitrary_p2pkh_script() -> ScriptBuf {
218266
let pubkey_hash = <[u8; 20]>::from_hex("16e1ae70ff0fa102905d4af297f6912bda6cce19").unwrap();
@@ -225,7 +273,6 @@ fn arbitrary_p2pkh_script() -> ScriptBuf {
225273
.push_opcode(OP_CHECKSIG)
226274
.into_script()
227275
}
228-
229276
fn arbitrary_multisig_script() -> ScriptBuf {
230277
let pk1 =
231278
<[u8; 33]>::from_hex("022afc20bf379bc96a2f4e9e63ffceb8652b2b6a097f63fbee6ecec2a49a48010e")
@@ -244,6 +291,123 @@ fn arbitrary_multisig_script() -> ScriptBuf {
244291
.push_opcode(OP_CHECKMULTISIG)
245292
.into_script()
246293
}
294+
fn arbitrary_p2wpkh_script() -> ScriptBuf {
295+
let pubkey = arbitrary_pubkey();
296+
let pubkey_hash = hash160::Hash::hash(&pubkey.to_bytes());
297+
298+
// P2WPKH: 0 <20-byte pubkey hash>
299+
Builder::new()
300+
.push_int(0)
301+
.push_slice(pubkey_hash.as_byte_array())
302+
.into_script()
303+
}
304+
305+
fn arbitrary_p2wsh_script() -> ScriptBuf {
306+
let redeem_script = arbitrary_multisig_script(); // any witness script
307+
let script_hash = sha256::Hash::hash(redeem_script.as_bytes());
308+
309+
// P2WSH: 0 <32-byte script hash>
310+
Builder::new()
311+
.push_int(0)
312+
.push_slice(script_hash.as_byte_array())
313+
.into_script()
314+
}
315+
316+
fn arbitrary_p2tr_script() -> ScriptBuf {
317+
let secp = Secp256k1::new();
318+
let sk = secp256k1::SecretKey::from_slice(&[2u8; 32]).unwrap();
319+
let internal_key = secp256k1::PublicKey::from_secret_key(&secp, &sk);
320+
let x_only = XOnlyPublicKey::from(internal_key);
321+
322+
// Taproot output script: OP_1 <x-only pubkey>
323+
Builder::new()
324+
.push_int(1)
325+
.push_slice(&x_only.serialize())
326+
.into_script()
327+
}
328+
329+
#[test]
330+
fn raw_transactions__decode_script_segwit__modelled() {
331+
332+
let node = Node::with_wallet(Wallet::Default, &["-txindex"]);
333+
node.client.load_wallet("default").ok(); // Ensure wallet is loaded
334+
node.fund_wallet();
335+
336+
// Get a new address and script
337+
let address_unc = node
338+
.client
339+
.get_new_address(None, None)
340+
.expect("getnewaddress")
341+
.address()
342+
.expect("valid address string");
343+
344+
let address = address_unc
345+
.require_network(Network::Regtest)
346+
.expect("must be regtest");
347+
348+
assert!(
349+
address.is_segwit(),
350+
"Expected SegWit address but got {:?}",
351+
address
352+
);
353+
354+
let script = address.script_pubkey();
355+
let hex = script.to_hex_string();
356+
357+
// Decode script
358+
let json = node.client.decode_script(&hex).expect("decodescript");
359+
let model: Result<mtype::DecodeScript, DecodeScriptError> = json.into_model();
360+
let decoded = model.expect("DecodeScript into model");
361+
362+
let segwit = decoded
363+
.segwit
364+
.as_ref()
365+
.expect("Expected segwit field to be present");
366+
367+
assert_eq!(
368+
segwit.hex, script,
369+
"Segwit hex does not match script"
370+
);
371+
372+
// Extract the type field
373+
let script_type = decoded
374+
.segwit
375+
.as_ref()
376+
.map(|s| s.type_.as_str())
377+
.unwrap_or_else(|| decoded.type_.as_str());
378+
379+
assert_eq!(
380+
script_type,
381+
"witness_v0_keyhash",
382+
"Expected script type to be witness_v0_keyhash"
383+
);
384+
385+
// Compare hex from segwit
386+
let decoded_hex = decoded
387+
.segwit
388+
.as_ref()
389+
.map(|s| &s.hex)
390+
.unwrap_or_else(|| {
391+
panic!("Expected segwit hex to be present")
392+
});
393+
394+
assert_eq!(*decoded_hex, script, "Script hex does not match");
395+
396+
// Compare addresses from segwit or fallback
397+
let address_unc_check = address.into_unchecked();
398+
let segwit_addresses = decoded
399+
.segwit
400+
.as_ref()
401+
.map(|s| &s.addresses)
402+
.unwrap_or(&decoded.addresses);
403+
404+
assert!(
405+
segwit_addresses.iter().any(|a| a == &address_unc_check),
406+
"Expected address {:?} in segwit.addresses or top-level addresses: {:?}",
407+
address_unc_check,
408+
segwit_addresses
409+
);
410+
}
247411

248412
#[test]
249413
fn raw_transactions__finalize_psbt__modelled() {

types/src/model/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub use self::{
4141
raw_transactions::{
4242
AnalyzePsbt, AnalyzePsbtInput, AnalyzePsbtInputMissing, CombinePsbt, CombineRawTransaction,
4343
ConvertToPsbt, CreatePsbt, CreateRawTransaction, DecodePsbt, DecodeRawTransaction,
44-
DecodeScript, DescriptorProcessPsbt, FinalizePsbt, FundRawTransaction, GetRawTransaction,
44+
DecodeScript, DecodeScriptSegwit, DescriptorProcessPsbt, FinalizePsbt, FundRawTransaction, GetRawTransaction,
4545
GetRawTransactionVerbose, JoinPsbts, MempoolAcceptance, MempoolAcceptanceFees,
4646
SendRawTransaction, SignFail, SignRawTransaction, SignRawTransactionWithKey, SubmitPackage,
4747
SubmitPackageTxResult, SubmitPackageTxResultFees, TestMempoolAccept, UtxoUpdatePsbt,

types/src/model/raw_transactions.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,29 @@ pub struct DecodeScript {
116116
pub addresses: Vec<Address<NetworkUnchecked>>,
117117
/// Address of P2SH script wrapping this redeem script (not returned if the script is already a P2SH).
118118
pub p2sh: Option<Address<NetworkUnchecked>>,
119-
/// Address of the P2SH script wrapping this witness redeem script
120-
pub p2sh_segwit: Option<String>,
119+
/// Result of a witness output script wrapping this redeem script (not returned for types that should not be wrapped).
120+
pub segwit: Option<DecodeScriptSegwit>,
121+
}
122+
/// Models the `segwit` field returned by the `decodescript` RPC.
123+
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
124+
#[serde(deny_unknown_fields)]
125+
pub struct DecodeScriptSegwit {
126+
/// Disassembly of the script.
127+
pub asm: String,
128+
/// The raw output script bytes, hex-encoded.
129+
pub hex: ScriptBuf,
130+
/// The output type (e.g. nonstandard, anchor, pubkey, pubkeyhash, scripthash, multisig, nulldata, witness_v0_scripthash, witness_v0_keyhash, witness_v1_taproot, witness_unknown).
131+
pub type_: String,
132+
/// Bitcoin address (only if a well-defined address exists)v22 and later only.
133+
pub address: Option<Address<NetworkUnchecked>>,
134+
/// The required signatures.
135+
pub required_signatures: Option<u64>,
136+
/// List of bitcoin addresses.
137+
pub addresses: Vec<Address<NetworkUnchecked>>,
138+
/// Inferred descriptor for the script. v23 and later only.
139+
pub descriptor: Option<String>,
140+
/// Address of the P2SH script wrapping this witness redeem script.
141+
pub p2sh_segwit: Option<Address<NetworkUnchecked>>,
121142
}
122143

123144
/// Models the result of JSON-RPC method `descriptorprocesspsbt`.

types/src/v17/raw_transactions/error.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ pub enum DecodeScriptError {
178178
Addresses(address::ParseError),
179179
/// Conversion of the transaction `p2sh` field failed.
180180
P2sh(address::ParseError),
181+
/// Conversion of the transaction `segwit` field failed.
182+
Segwit(DecodeScriptSegwitError),
181183
}
182184

183185
impl fmt::Display for DecodeScriptError {
@@ -188,6 +190,7 @@ impl fmt::Display for DecodeScriptError {
188190
E::Hex(ref e) => write_err!(f, "conversion of the `hex` field failed"; e),
189191
E::Addresses(ref e) => write_err!(f, "conversion of the `addresses` field failed"; e),
190192
E::P2sh(ref e) => write_err!(f, "conversion of the `p2sh` field failed"; e),
193+
E::Segwit(ref e) => write_err!(f, "conversion of the `segwit` field failed"; e),
191194
}
192195
}
193196
}
@@ -201,6 +204,34 @@ impl std::error::Error for DecodeScriptError {
201204
E::Hex(ref e) => Some(e),
202205
E::Addresses(ref e) => Some(e),
203206
E::P2sh(ref e) => Some(e),
207+
E::Segwit(ref e) => Some(e),
208+
}
209+
}
210+
}
211+
212+
/// Error when converting a `DecodeScriptSegwit` type into the model type.
213+
#[derive(Debug)]
214+
pub enum DecodeScriptSegwitError {
215+
/// Conversion of the transaction `addresses` field failed.
216+
Addresses(address::ParseError),
217+
}
218+
219+
impl fmt::Display for DecodeScriptSegwitError {
220+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
221+
use DecodeScriptSegwitError as E;
222+
match *self {
223+
E::Addresses(ref e) =>
224+
write_err!(f, "conversion of the `addresses` field in `segwit` failed"; e),
225+
}
226+
}
227+
}
228+
229+
#[cfg(feature = "std")]
230+
impl std::error::Error for DecodeScriptSegwitError {
231+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
232+
use DecodeScriptSegwitError as E;
233+
match *self {
234+
E::Addresses(ref e) => Some(e),
204235
}
205236
}
206237
}

types/src/v17/raw_transactions/into.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ use bitcoin::{
1111
use super::{
1212
CombinePsbt, CombineRawTransaction, ConvertToPsbt, CreatePsbt, CreateRawTransaction,
1313
DecodePsbt, DecodePsbtError, DecodeRawTransaction, DecodeScript, DecodeScriptError,
14-
FinalizePsbt, FinalizePsbtError, FundRawTransaction, FundRawTransactionError,
15-
GetRawTransaction, GetRawTransactionVerbose, GetRawTransactionVerboseError, MempoolAcceptance,
16-
PsbtInput, PsbtInputError, PsbtOutput, PsbtOutputError, SendRawTransaction, SignFail,
17-
SignFailError, SignRawTransaction, SignRawTransactionError, TestMempoolAccept,
14+
DecodeScriptSegwit, DecodeScriptSegwitError, FinalizePsbt, FinalizePsbtError,
15+
FundRawTransaction, FundRawTransactionError, GetRawTransaction, GetRawTransactionVerbose,
16+
GetRawTransactionVerboseError, MempoolAcceptance, PsbtInput, PsbtInputError, PsbtOutput,
17+
PsbtOutputError, SendRawTransaction, SignFail, SignFailError, SignRawTransaction,
18+
SignRawTransactionError, TestMempoolAccept,
1819
};
1920
use crate::model;
2021
use crate::psbt::RawTransactionError;
@@ -309,7 +310,38 @@ impl DecodeScript {
309310
required_signatures: self.required_signatures,
310311
addresses,
311312
p2sh,
312-
p2sh_segwit: self.p2sh_segwit,
313+
segwit: None,
314+
})
315+
}
316+
}
317+
#[allow(dead_code)]
318+
impl DecodeScriptSegwit {
319+
/// Converts version specific type to a version nonspecific, more strongly typed type.
320+
pub fn into_model(self) -> Result<model::DecodeScriptSegwit, DecodeScriptSegwitError> {
321+
use DecodeScriptSegwitError as E;
322+
323+
// Convert `Option<Vec<String>>` to `Vec<Address<NetworkUnchecked>>`
324+
let addresses = match self.addresses {
325+
Some(addrs) => addrs
326+
.into_iter()
327+
.map(|s| s.parse::<Address<_>>())
328+
.collect::<Result<_, _>>()
329+
.map_err(E::Addresses)?,
330+
None => vec![],
331+
};
332+
333+
let required_signatures = self.required_signatures;
334+
let p2sh_segwit = self.p2sh_segwit;
335+
336+
Ok(model::DecodeScriptSegwit {
337+
asm: self.asm,
338+
hex: self.hex,
339+
descriptor: None,
340+
address: None,
341+
type_: self.type_,
342+
required_signatures,
343+
addresses,
344+
p2sh_segwit,
313345
})
314346
}
315347
}

0 commit comments

Comments
 (0)