Skip to content

Commit 4c5e244

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 87c72e7 commit 4c5e244

File tree

18 files changed

+633
-102
lines changed

18 files changed

+633
-102
lines changed

integration_test/tests/raw_transactions.rs

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

1823
#[test]
1924
#[cfg(not(feature = "v17"))] // analyzepsbt was added in v0.18.
@@ -196,24 +201,72 @@ fn raw_transactions__decode_raw_transaction__modelled() {
196201
}
197202

198203
#[test]
199-
// FIXME: Seems the returned fields are different depending on the script. Needs more thorough testing.
204+
// FIXME: Bitcoin Core may populate different fields depending on
205+
// the script type and Core version (e.g. legacy vs segwit vs taproot).
200206
fn raw_transactions__decode_script__modelled() {
201207
let node = Node::with_wallet(Wallet::Default, &["-txindex"]);
202208
node.fund_wallet();
203209

204-
let p2pkh = arbitrary_p2pkh_script();
205-
let multi = arbitrary_multisig_script();
206-
207-
for script in &[p2pkh, multi] {
208-
let hex = script.to_hex_string();
209-
210-
let json: DecodeScript = node.client.decode_script(&hex).expect("decodescript");
211-
let model: Result<mtype::DecodeScript, DecodeScriptError> = json.into_model();
212-
model.unwrap();
210+
let cases = [
211+
("p2pkh", arbitrary_p2pkh_script(), Some("pubkeyhash")),
212+
("multisig", arbitrary_multisig_script(), Some("multisig")),
213+
("p2sh", arbitrary_p2sh_script(), Some("scripthash")),
214+
("bare", arbitrary_bare_script(), Some("nonstandard")),
215+
("p2wpkh", arbitrary_p2wpkh_script(), Some("witness_v0_keyhash")),
216+
("p2wsh", arbitrary_p2wsh_script(), Some("witness_v0_scripthash")),
217+
("p2tr", arbitrary_p2tr_script(), Some("witness_v1_taproot")),
218+
];
219+
220+
for (label, script, expected_type) in cases {
221+
// The input is provided as raw script hex, not an address.
222+
let json: DecodeScript =
223+
node.client.decode_script(&script.to_hex_string())
224+
.expect("decodescript");
225+
226+
// Convert the RPC response into the model type.
227+
// This step normalizes Core’s JSON into a structured representation.
228+
let decoded = json
229+
.into_model()
230+
.expect("DecodeScript into model");
231+
232+
// Verify that Core identifies the script type as expected.
233+
// Some scripts may legitimately omit type information depending on Core behavior.
234+
if let Some(expected) = expected_type {
235+
assert_eq!(
236+
decoded.type_, expected,
237+
"Unexpected script type for {label}"
238+
);
239+
}
240+
241+
// For standard scripts, Core should provide at least one resolved address.
242+
// Core may populate either `address` or `addresses`, depending on script class.
243+
if expected_type != Some("nonstandard") {
244+
assert!(
245+
!decoded.addresses.is_empty() || decoded.address.is_some(),
246+
"Expected at least one address for {label}"
247+
);
248+
}
213249
}
214250
}
251+
fn arbitrary_p2sh_script() -> ScriptBuf {
252+
let redeem_script = arbitrary_multisig_script(); // or arbitrary_p2pkh_script()
253+
let redeem_script_hash = hash160::Hash::hash(redeem_script.as_bytes());
254+
255+
script::Builder::new()
256+
.push_opcode(bitcoin::opcodes::all::OP_HASH160)
257+
.push_slice(redeem_script_hash.as_byte_array())
258+
.push_opcode(bitcoin::opcodes::all::OP_EQUAL)
259+
.into_script()
260+
}
261+
fn arbitrary_bare_script() -> ScriptBuf {
262+
script::Builder::new().push_opcode(OP_RETURN).push_slice(b"hello").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+
}
215269

216-
// Script builder code copied from rust-bitcoin script unit tests.
217270
fn arbitrary_p2pkh_script() -> ScriptBuf {
218271
let pubkey_hash = <[u8; 20]>::from_hex("16e1ae70ff0fa102905d4af297f6912bda6cce19").unwrap();
219272

@@ -225,7 +278,6 @@ fn arbitrary_p2pkh_script() -> ScriptBuf {
225278
.push_opcode(OP_CHECKSIG)
226279
.into_script()
227280
}
228-
229281
fn arbitrary_multisig_script() -> ScriptBuf {
230282
let pk1 =
231283
<[u8; 33]>::from_hex("022afc20bf379bc96a2f4e9e63ffceb8652b2b6a097f63fbee6ecec2a49a48010e")
@@ -244,6 +296,96 @@ fn arbitrary_multisig_script() -> ScriptBuf {
244296
.push_opcode(OP_CHECKMULTISIG)
245297
.into_script()
246298
}
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().push_int(0).push_slice(pubkey_hash.as_byte_array()).into_script()
305+
}
306+
307+
fn arbitrary_p2wsh_script() -> ScriptBuf {
308+
let redeem_script = arbitrary_multisig_script(); // any witness script
309+
let script_hash = sha256::Hash::hash(redeem_script.as_bytes());
310+
311+
// P2WSH: 0 <32-byte script hash>
312+
Builder::new().push_int(0).push_slice(script_hash.as_byte_array()).into_script()
313+
}
314+
315+
fn arbitrary_p2tr_script() -> ScriptBuf {
316+
let secp = Secp256k1::new();
317+
let sk = secp256k1::SecretKey::from_slice(&[2u8; 32]).unwrap();
318+
let internal_key = secp256k1::PublicKey::from_secret_key(&secp, &sk);
319+
let x_only = XOnlyPublicKey::from(internal_key);
320+
321+
// Taproot output script: OP_1 <x-only pubkey>
322+
Builder::new().push_int(1).push_slice(&x_only.serialize()).into_script()
323+
}
324+
325+
#[test]
326+
fn raw_transactions__decode_script_segwit__modelled() {
327+
let node = Node::with_wallet(Wallet::Default, &["-txindex"]);
328+
node.client.load_wallet("default").ok();
329+
node.fund_wallet();
330+
331+
let address = node
332+
.client
333+
.get_new_address(None, None)
334+
.expect("getnewaddress")
335+
.address()
336+
.expect("valid address")
337+
.require_network(Network::Regtest)
338+
.expect("regtest");
339+
340+
// Convert the address into its locking script (scriptPubKey).
341+
// We assert on the script itself (not the address encoding) to ensure
342+
// we are testing actual SegWit script semantics.
343+
let spk = address.script_pubkey();
344+
assert!(
345+
spk.is_witness_program(),
346+
"Expected segwit script"
347+
);
348+
349+
// Decode the script and convert it into the model type.
350+
// Core may populate fields differently depending on script type and version.
351+
let decoded = node
352+
.client
353+
.decode_script(&spk.to_hex_string())
354+
.expect("decodescript")
355+
.into_model()
356+
.expect("DecodeScript into model");
357+
358+
// For SegWit scripts, Core should populate the `segwit` sub-object.
359+
let segwit = decoded
360+
.segwit
361+
.as_ref()
362+
.expect("Expected segwit field");
363+
364+
// The decoded SegWit script hex must match the original scriptPubKey.
365+
assert_eq!(segwit.hex, spk);
366+
367+
// Verify that Core correctly identifies the SegWit version and script type.
368+
// For a wallet-generated address on regtest, this should be v0 P2WPKH.
369+
assert_eq!(
370+
segwit.type_.as_str(),
371+
"witness_v0_keyhash",
372+
"Unexpected segwit script type"
373+
);
374+
375+
// Core returns addresses without network information.
376+
// We compare against the unchecked form of the address for correctness.
377+
let addr_unc = address.into_unchecked();
378+
let addresses = &segwit.addresses;
379+
380+
// Ensure the decoded SegWit script resolves back to the original address.
381+
assert!(
382+
addresses.contains(&addr_unc),
383+
"Expected address {:?} in segwit.addresses: {:?}",
384+
addr_unc,
385+
addresses
386+
);
387+
}
388+
247389

248390
#[test]
249391
fn raw_transactions__finalize_psbt__modelled() {

types/src/model/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,11 @@ pub use self::{
4646
raw_transactions::{
4747
AnalyzePsbt, AnalyzePsbtInput, AnalyzePsbtInputMissing, CombinePsbt, CombineRawTransaction,
4848
ConvertToPsbt, CreatePsbt, CreateRawTransaction, DecodePsbt, DecodeRawTransaction,
49-
DecodeScript, DescriptorProcessPsbt, FinalizePsbt, FundRawTransaction, GetRawTransaction,
50-
GetRawTransactionVerbose, JoinPsbts, MempoolAcceptance, MempoolAcceptanceFees,
51-
SendRawTransaction, SignFail, SignRawTransaction, SignRawTransactionWithKey, SubmitPackage,
52-
SubmitPackageTxResult, SubmitPackageTxResultFees, TestMempoolAccept, UtxoUpdatePsbt,
49+
DecodeScript, DecodeScriptSegwit, DescriptorProcessPsbt, FinalizePsbt, FundRawTransaction,
50+
GetRawTransaction, GetRawTransactionVerbose, JoinPsbts, MempoolAcceptance,
51+
MempoolAcceptanceFees, SendRawTransaction, SignFail, SignRawTransaction,
52+
SignRawTransactionWithKey, SubmitPackage, SubmitPackageTxResult, SubmitPackageTxResultFees,
53+
TestMempoolAccept, UtxoUpdatePsbt,
5354
},
5455
util::{
5556
CreateMultisig, DeriveAddresses, DeriveAddressesMultipath, EstimateSmartFee,

types/src/model/raw_transactions.rs

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

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

types/src/v17/raw_transactions/error.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,26 +167,58 @@ pub enum DecodeScriptError {
167167
Addresses(address::ParseError),
168168
/// Conversion of the transaction `p2sh` field failed.
169169
P2sh(address::ParseError),
170+
/// Conversion of the transaction `segwit` field failed.
171+
Segwit(DecodeScriptSegwitError),
170172
}
171173

172174
impl fmt::Display for DecodeScriptError {
173175
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176+
use DecodeScriptError as E;
174177
match *self {
175-
Self::Hex(ref e) => write_err!(f, "conversion of the `hex` field failed"; e),
176-
Self::Addresses(ref e) =>
177-
write_err!(f, "conversion of the `addresses` field failed"; e),
178-
Self::P2sh(ref e) => write_err!(f, "conversion of the `p2sh` field failed"; e),
178+
E::Hex(ref e) => write_err!(f, "conversion of the `hex` field failed"; e),
179+
E::Addresses(ref e) => write_err!(f, "conversion of the `addresses` field failed"; e),
180+
E::P2sh(ref e) => write_err!(f, "conversion of the `p2sh` field failed"; e),
181+
E::Segwit(ref e) => write_err!(f, "conversion of the `segwit` field failed"; e),
179182
}
180183
}
181184
}
182185

183186
#[cfg(feature = "std")]
184187
impl std::error::Error for DecodeScriptError {
185188
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
189+
use DecodeScriptError as E;
186190
match *self {
187-
Self::Hex(ref e) => Some(e),
188-
Self::Addresses(ref e) => Some(e),
189-
Self::P2sh(ref e) => Some(e),
191+
E::Hex(ref e) => Some(e),
192+
E::Addresses(ref e) => Some(e),
193+
E::P2sh(ref e) => Some(e),
194+
E::Segwit(ref e) => Some(e),
195+
}
196+
}
197+
}
198+
199+
/// Error when converting a `DecodeScriptSegwit` type into the model type.
200+
#[derive(Debug)]
201+
pub enum DecodeScriptSegwitError {
202+
/// Conversion of the transaction `addresses` field failed.
203+
Addresses(address::ParseError),
204+
}
205+
206+
impl fmt::Display for DecodeScriptSegwitError {
207+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
208+
use DecodeScriptSegwitError as E;
209+
match *self {
210+
E::Addresses(ref e) =>
211+
write_err!(f, "conversion of the `addresses` field in `segwit` failed"; e),
212+
}
213+
}
214+
}
215+
216+
#[cfg(feature = "std")]
217+
impl std::error::Error for DecodeScriptSegwitError {
218+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
219+
use DecodeScriptSegwitError as E;
220+
match *self {
221+
E::Addresses(ref e) => Some(e),
190222
}
191223
}
192224
}

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)