Skip to content

Commit d853c19

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 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
1 parent 2d0c090 commit d853c19

File tree

18 files changed

+631
-93
lines changed

18 files changed

+631
-93
lines changed

integration_test/tests/raw_transactions.rs

Lines changed: 162 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@
44
55
#![allow(non_snake_case)] // Test names intentionally use double underscore.
66
#![allow(unused_imports)] // Because of feature gated tests.
7-
8-
use bitcoin::hex::FromHex as _;
9-
use bitcoin::opcodes::all::*;
10-
use bitcoin::{absolute, transaction, consensus, script, Amount, TxOut, Transaction, ScriptBuf};
117
use integration_test::{Node, NodeExt as _, Wallet};
128
use node::{mtype, Input, Output};
139
use node::vtype::*; // All the version specific types.
10+
use bitcoin::{hex::FromHex as _,
11+
absolute, transaction, consensus,Amount, TxOut, Transaction,
12+
Address, Network, ScriptBuf,script, hashes::{hash160,sha256,Hash},
13+
WPubkeyHash, WScriptHash, secp256k1,
14+
PublicKey,
15+
script::Builder,
16+
opcodes::all::*,
17+
key::{Secp256k1, XOnlyPublicKey},
18+
address::NetworkUnchecked,
19+
};
20+
use rand::Rng;
21+
1422

1523
#[test]
1624
#[cfg(not(feature = "v17"))] // analyzepsbt was added in v0.18.
@@ -202,18 +210,62 @@ fn raw_transactions__decode_script__modelled() {
202210
let node = Node::with_wallet(Wallet::Default, &["-txindex"]);
203211
node.fund_wallet();
204212

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

211226
let json: DecodeScript = node.client.decode_script(&hex).expect("decodescript");
212227
let model: Result<mtype::DecodeScript, DecodeScriptError> = json.into_model();
213-
let _ = model.expect("DecodeScript into model");
228+
let decoded = model.expect("DecodeScript into model");
229+
230+
println!("Decoded script ({label}): {:?}", decoded);
231+
232+
if let Some(expected) = expected_type {
233+
assert_eq!(decoded.type_, expected, "Unexpected script type for {label}");
234+
} else {
235+
println!("Skipping type check for {}", label);
236+
}
237+
238+
// Address should be present for standard scripts
239+
if expected_type != Some("nonstandard") {
240+
assert!(
241+
!decoded.addresses.is_empty(),
242+
"Expected at least one address for {label}"
243+
);
244+
}
214245
}
215246
}
247+
fn arbitrary_p2sh_script() -> ScriptBuf {
248+
249+
let redeem_script = arbitrary_multisig_script(); // or arbitrary_p2pkh_script()
250+
let redeem_script_hash = hash160::Hash::hash(redeem_script.as_bytes());
216251

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

249401
#[test]
250402
#[cfg(feature = "TODO")]

types/src/model/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ pub use self::{
4343
raw_transactions::{
4444
AnalyzePsbt, AnalyzePsbtInput, AnalyzePsbtInputMissing, CombinePsbt, CombineRawTransaction,
4545
ConvertToPsbt, CreatePsbt, CreateRawTransaction, DecodePsbt, DecodeRawTransaction,
46-
DecodeScript, DescriptorProcessPsbt, FinalizePsbt, FundRawTransaction, GetRawTransaction,
47-
GetRawTransactionVerbose, JoinPsbts, MempoolAcceptance, SendRawTransaction, SignFail,
48-
SignRawTransaction, SubmitPackage, SubmitPackageTxResult, SubmitPackageTxResultFees,
49-
TestMempoolAccept, UtxoUpdatePsbt,
46+
DecodeScript, DecodeScriptSegwit, DescriptorProcessPsbt, FinalizePsbt, FundRawTransaction,
47+
GetRawTransaction, GetRawTransactionVerbose, JoinPsbts, MempoolAcceptance,
48+
SendRawTransaction, SignFail, SignRawTransaction, SubmitPackage, SubmitPackageTxResult,
49+
SubmitPackageTxResultFees, TestMempoolAccept, UtxoUpdatePsbt,
5050
},
5151
util::{
5252
CreateMultisig, DeriveAddresses, EstimateSmartFee, SignMessageWithPrivKey, ValidateAddress,

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)