Skip to content

Commit 0fbba1a

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 889a2e3 commit 0fbba1a

File tree

18 files changed

+631
-118
lines changed

18 files changed

+631
-118
lines changed

integration_test/tests/raw_transactions.rs

Lines changed: 133 additions & 8 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.
@@ -201,18 +206,56 @@ fn raw_transactions__decode_script__modelled() {
201206
let node = Node::with_wallet(Wallet::Default, &["-txindex"]);
202207
node.fund_wallet();
203208

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

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

245+
script::Builder::new()
246+
.push_opcode(bitcoin::opcodes::all::OP_HASH160)
247+
.push_slice(redeem_script_hash.as_byte_array()) // [u8; 20]
248+
.push_opcode(bitcoin::opcodes::all::OP_EQUAL)
249+
.into_script()
250+
}
251+
fn arbitrary_bare_script() -> ScriptBuf {
252+
script::Builder::new().push_opcode(OP_RETURN).push_slice(b"hello").into_script()
253+
}
254+
fn arbitrary_pubkey() -> PublicKey {
255+
let secp = Secp256k1::new();
256+
let secret_key = secp256k1::SecretKey::from_slice(&[1u8; 32]).unwrap();
257+
PublicKey::new(secp256k1::PublicKey::from_secret_key(&secp, &secret_key))
258+
}
216259
// Script builder code copied from rust-bitcoin script unit tests.
217260
fn arbitrary_p2pkh_script() -> ScriptBuf {
218261
let pubkey_hash = <[u8; 20]>::from_hex("16e1ae70ff0fa102905d4af297f6912bda6cce19").unwrap();
@@ -225,7 +268,6 @@ fn arbitrary_p2pkh_script() -> ScriptBuf {
225268
.push_opcode(OP_CHECKSIG)
226269
.into_script()
227270
}
228-
229271
fn arbitrary_multisig_script() -> ScriptBuf {
230272
let pk1 =
231273
<[u8; 33]>::from_hex("022afc20bf379bc96a2f4e9e63ffceb8652b2b6a097f63fbee6ecec2a49a48010e")
@@ -244,6 +286,89 @@ fn arbitrary_multisig_script() -> ScriptBuf {
244286
.push_opcode(OP_CHECKMULTISIG)
245287
.into_script()
246288
}
289+
fn arbitrary_p2wpkh_script() -> ScriptBuf {
290+
let pubkey = arbitrary_pubkey();
291+
let pubkey_hash = hash160::Hash::hash(&pubkey.to_bytes());
292+
293+
// P2WPKH: 0 <20-byte pubkey hash>
294+
Builder::new().push_int(0).push_slice(pubkey_hash.as_byte_array()).into_script()
295+
}
296+
297+
fn arbitrary_p2wsh_script() -> ScriptBuf {
298+
let redeem_script = arbitrary_multisig_script(); // any witness script
299+
let script_hash = sha256::Hash::hash(redeem_script.as_bytes());
300+
301+
// P2WSH: 0 <32-byte script hash>
302+
Builder::new().push_int(0).push_slice(script_hash.as_byte_array()).into_script()
303+
}
304+
305+
fn arbitrary_p2tr_script() -> ScriptBuf {
306+
let secp = Secp256k1::new();
307+
let sk = secp256k1::SecretKey::from_slice(&[2u8; 32]).unwrap();
308+
let internal_key = secp256k1::PublicKey::from_secret_key(&secp, &sk);
309+
let x_only = XOnlyPublicKey::from(internal_key);
310+
311+
// Taproot output script: OP_1 <x-only pubkey>
312+
Builder::new().push_int(1).push_slice(&x_only.serialize()).into_script()
313+
}
314+
315+
#[test]
316+
fn raw_transactions__decode_script_segwit__modelled() {
317+
let node = Node::with_wallet(Wallet::Default, &["-txindex"]);
318+
node.client.load_wallet("default").ok(); // Ensure wallet is loaded
319+
node.fund_wallet();
320+
321+
// Get a new address and script
322+
let address_unc = node
323+
.client
324+
.get_new_address(None, None)
325+
.expect("getnewaddress")
326+
.address()
327+
.expect("valid address string");
328+
329+
let address = address_unc.require_network(Network::Regtest).expect("must be regtest");
330+
331+
assert!(address.is_segwit(), "Expected SegWit address but got {:?}", address);
332+
333+
let script = address.script_pubkey();
334+
let hex = script.to_hex_string();
335+
336+
// Decode script
337+
let json = node.client.decode_script(&hex).expect("decodescript");
338+
let model: Result<mtype::DecodeScript, DecodeScriptError> = json.into_model();
339+
let decoded = model.expect("DecodeScript into model");
340+
341+
let segwit = decoded.segwit.as_ref().expect("Expected segwit field to be present");
342+
343+
assert_eq!(segwit.hex, script, "Segwit hex does not match script");
344+
345+
// Extract the type field
346+
let script_type =
347+
decoded.segwit.as_ref().map(|s| s.type_.as_str()).unwrap_or_else(|| decoded.type_.as_str());
348+
349+
assert_eq!(script_type, "witness_v0_keyhash", "Expected script type to be witness_v0_keyhash");
350+
351+
// Compare hex from segwit
352+
let decoded_hex = decoded
353+
.segwit
354+
.as_ref()
355+
.map(|s| &s.hex)
356+
.unwrap_or_else(|| panic!("Expected segwit hex to be present"));
357+
358+
assert_eq!(*decoded_hex, script, "Script hex does not match");
359+
360+
// Compare addresses from segwit or fallback
361+
let address_unc_check = address.into_unchecked();
362+
let segwit_addresses =
363+
decoded.segwit.as_ref().map(|s| &s.addresses).unwrap_or(&decoded.addresses);
364+
365+
assert!(
366+
segwit_addresses.iter().any(|a| a == &address_unc_check),
367+
"Expected address {:?} in segwit.addresses or top-level addresses: {:?}",
368+
address_unc_check,
369+
segwit_addresses
370+
);
371+
}
247372

248373
#[test]
249374
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
@@ -45,10 +45,11 @@ pub use self::{
4545
raw_transactions::{
4646
AnalyzePsbt, AnalyzePsbtInput, AnalyzePsbtInputMissing, CombinePsbt, CombineRawTransaction,
4747
ConvertToPsbt, CreatePsbt, CreateRawTransaction, DecodePsbt, DecodeRawTransaction,
48-
DecodeScript, DescriptorProcessPsbt, FinalizePsbt, FundRawTransaction, GetRawTransaction,
49-
GetRawTransactionVerbose, JoinPsbts, MempoolAcceptance, MempoolAcceptanceFees,
50-
SendRawTransaction, SignFail, SignRawTransaction, SignRawTransactionWithKey, SubmitPackage,
51-
SubmitPackageTxResult, SubmitPackageTxResultFees, TestMempoolAccept, UtxoUpdatePsbt,
48+
DecodeScript, DecodeScriptSegwit, DescriptorProcessPsbt, FinalizePsbt, FundRawTransaction,
49+
GetRawTransaction, GetRawTransactionVerbose, JoinPsbts, MempoolAcceptance,
50+
MempoolAcceptanceFees, SendRawTransaction, SignFail, SignRawTransaction,
51+
SignRawTransactionWithKey, SubmitPackage, SubmitPackageTxResult, SubmitPackageTxResultFees,
52+
TestMempoolAccept, UtxoUpdatePsbt,
5253
},
5354
util::{
5455
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)