Skip to content

Commit 9f72bb3

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add network parameter to output script validation
Add proper validation for supported script types based on network capabilities. This adds checks for SegWit and Taproot support in the output script generation methods, ensuring scripts are only created for networks that support them. - Add OutputScriptSupport struct to verify script compatibility - Add network parameter to outputScript function - Implement per-network script validation logic - Document supported features across different networks Issue: BTC-2652 Co-authored-by: llm-git <[email protected]>
1 parent d3f259e commit 9f72bb3

File tree

6 files changed

+163
-22
lines changed

6 files changed

+163
-22
lines changed

packages/wasm-utxo/js/fixedScriptWallet.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export type WalletKeys =
1212
/**
1313
* Create the output script for a given wallet keys and chain and index
1414
*/
15-
export function outputScript(keys: WalletKeys, chain: number, index: number): Uint8Array {
16-
return FixedScriptWalletNamespace.output_script(keys, chain, index);
15+
export function outputScript(keys: WalletKeys, chain: number, index: number, network: UtxolibNetwork): Uint8Array {
16+
return FixedScriptWalletNamespace.output_script(keys, chain, index, network);
1717
}
1818

1919
/**

packages/wasm-utxo/src/address/networks.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,106 @@ impl AddressFormat {
7171
}
7272
}
7373

74+
pub struct OutputScriptSupport {
75+
pub segwit: bool,
76+
pub taproot: bool,
77+
}
78+
79+
impl OutputScriptSupport {
80+
pub(crate) fn assert_legacy(&self) -> Result<()> {
81+
// all coins support legacy scripts
82+
Ok(())
83+
}
84+
85+
pub(crate) fn assert_segwit(&self) -> Result<()> {
86+
if !self.segwit {
87+
return Err(AddressError::UnsupportedScriptType(
88+
"Network does not support segwit".to_string(),
89+
));
90+
}
91+
Ok(())
92+
}
93+
94+
pub(crate) fn assert_taproot(&self) -> Result<()> {
95+
if !self.taproot {
96+
return Err(AddressError::UnsupportedScriptType(
97+
"Network does not support taproot".to_string(),
98+
));
99+
}
100+
Ok(())
101+
}
102+
103+
pub fn assert_support(&self, script: &Script) -> Result<()> {
104+
match script.witness_version() {
105+
None => {
106+
// all coins support legacy scripts
107+
}
108+
Some(WitnessVersion::V0) => {
109+
self.assert_segwit()?;
110+
}
111+
Some(WitnessVersion::V1) => {
112+
self.assert_taproot()?;
113+
}
114+
_ => {
115+
return Err(AddressError::UnsupportedScriptType(
116+
"Unsupported witness version".to_string(),
117+
));
118+
}
119+
}
120+
Ok(())
121+
}
122+
}
123+
124+
impl Network {
125+
pub fn output_script_support(&self) -> OutputScriptSupport {
126+
// SegWit support:
127+
// Bitcoin: SegWit activated August 24, 2017 at block 481,824
128+
// - Consensus rules: https://github.com/bitcoin/bitcoin/blob/v28.0/src/consensus/tx_verify.cpp
129+
// - Witness validation: https://github.com/bitcoin/bitcoin/blob/v28.0/src/script/interpreter.cpp
130+
// - BIP141 (SegWit): https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
131+
// - BIP143 (Signature verification): https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
132+
// - BIP144 (P2P changes): https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki
133+
//
134+
// Litecoin: SegWit activated May 10, 2017 at block 1,201,536
135+
// - Consensus implementation: https://github.com/litecoin-project/litecoin/blob/v0.21.4/src/consensus/tx_verify.cpp
136+
// - Script interpreter: https://github.com/litecoin-project/litecoin/blob/v0.21.4/src/script/interpreter.cpp
137+
//
138+
// Bitcoin Gold: Launched with SegWit support in October 2017
139+
// - Implementation: https://github.com/BTCGPU/BTCGPU/blob/v0.17.3/src/consensus/tx_verify.cpp
140+
let segwit = matches!(
141+
self.mainnet(),
142+
Network::Bitcoin | Network::Litecoin | Network::BitcoinGold
143+
);
144+
145+
// Taproot support:
146+
// Bitcoin: Taproot activated November 14, 2021 at block 709,632
147+
// - Taproot validation: https://github.com/bitcoin/bitcoin/blob/v28.0/src/script/interpreter.cpp
148+
// (see VerifyWitnessProgram, WITNESS_V1_TAPROOT)
149+
// - Schnorr signature verification: https://github.com/bitcoin/bitcoin/blob/v28.0/src/pubkey.cpp
150+
// (see XOnlyPubKey::VerifySchnorr)
151+
// - Deployment params: https://github.com/bitcoin/bitcoin/blob/v28.0/src/kernel/chainparams.cpp
152+
// (see Consensus::DeploymentPos::DEPLOYMENT_TAPROOT)
153+
// - BIP340 (Schnorr signatures): https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
154+
// - BIP341 (Taproot): https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
155+
// - BIP342 (Tapscript): https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki
156+
//
157+
// Litecoin: has apparent taproot support, but we have not enabled it in this library yet.
158+
// - https://github.com/litecoin-project/litecoin/blob/v0.21.4/src/chainparams.cpp#L89-L92
159+
// - https://github.com/litecoin-project/litecoin/blob/v0.21.4/src/script/interpreter.h#L129-L131
160+
let taproot = segwit && matches!(self.mainnet(), Network::Bitcoin);
161+
162+
OutputScriptSupport { segwit, taproot }
163+
}
164+
}
165+
74166
/// Get codec for encoding an address for a given network and script type.
75167
fn get_encode_codec(
76168
network: Network,
77169
script: &Script,
78170
format: AddressFormat,
79171
) -> Result<&'static dyn AddressCodec> {
172+
network.output_script_support().assert_support(script)?;
173+
80174
let is_witness = script.is_p2wpkh() || script.is_p2wsh() || script.is_p2tr();
81175
let is_legacy = script.is_p2pkh() || script.is_p2sh();
82176

@@ -208,6 +302,7 @@ pub fn from_output_script_with_coin_and_format(
208302
from_output_script_with_network_and_format(script, network, format)
209303
}
210304

305+
use miniscript::bitcoin::WitnessVersion;
211306
// WASM bindings
212307
use wasm_bindgen::prelude::*;
213308

packages/wasm-utxo/src/address/utxolib_compat.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/// but for now we need to keep this compatibility layer.
44
use wasm_bindgen::JsValue;
55

6-
use crate::address::networks::AddressFormat;
6+
use crate::address::networks::{AddressFormat, OutputScriptSupport};
77
use crate::address::{bech32, cashaddr, Base58CheckCodec};
88
use crate::bitcoin::{Script, ScriptBuf};
99

@@ -31,6 +31,19 @@ impl Network {
3131
Network::try_from_js_value(js_network)
3232
.map_err(|e| AddressError::InvalidAddress(e.to_string()))
3333
}
34+
pub fn output_script_support(&self) -> OutputScriptSupport {
35+
let segwit = self.bech32.is_some();
36+
37+
// In the context of this library, only bitcoin supports taproot
38+
// See output_script_support in networks.rs for detailed references
39+
let taproot = segwit
40+
&& self
41+
.bech32
42+
.as_ref()
43+
.is_some_and(|bech32| bech32 == "bc" || bech32 == "tb");
44+
45+
OutputScriptSupport { segwit, taproot }
46+
}
3447
}
3548

3649
/// Convert output script to address string using a utxolib Network object
@@ -39,6 +52,8 @@ pub fn from_output_script_with_network(
3952
network: &Network,
4053
format: AddressFormat,
4154
) -> Result<String> {
55+
network.output_script_support().assert_support(script)?;
56+
4257
// Handle cashaddr format if requested
4358
if matches!(format, AddressFormat::Cashaddr) {
4459
if let Some(ref cash_addr) = network.cash_addr {

packages/wasm-utxo/src/fixed_script_wallet/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub use wallet_keys::*;
1212
pub use wallet_scripts::*;
1313
use wasm_bindgen::prelude::*;
1414

15-
use crate::address::networks::AddressFormat;
15+
use crate::address::networks::{AddressFormat};
1616
use crate::error::WasmMiniscriptError;
1717
use crate::try_from_js_value::TryFromJsValue;
1818
use crate::utxolib_compat::Network;
@@ -27,12 +27,14 @@ impl FixedScriptWalletNamespace {
2727
keys: JsValue,
2828
chain: u32,
2929
index: u32,
30+
network: JsValue,
3031
) -> Result<Vec<u8>, WasmMiniscriptError> {
32+
let network = Network::try_from_js_value(&network)?;
3133
let chain = Chain::try_from(chain)
3234
.map_err(|e| WasmMiniscriptError::new(&format!("Invalid chain: {}", e)))?;
3335

3436
let wallet_keys = RootWalletKeys::from_jsvalue(&keys)?;
35-
let scripts = WalletScripts::from_wallet_keys(&wallet_keys, chain, index);
37+
let scripts = WalletScripts::from_wallet_keys(&wallet_keys, chain, index, &network.output_script_support())?;
3638
Ok(scripts.output_script().to_bytes())
3739
}
3840

@@ -48,7 +50,7 @@ impl FixedScriptWalletNamespace {
4850
let wallet_keys = RootWalletKeys::from_jsvalue(&keys)?;
4951
let chain = Chain::try_from(chain)
5052
.map_err(|e| WasmMiniscriptError::new(&format!("Invalid chain: {}", e)))?;
51-
let scripts = WalletScripts::from_wallet_keys(&wallet_keys, chain, index);
53+
let scripts = WalletScripts::from_wallet_keys(&wallet_keys, chain, index, &network.output_script_support())?;
5254
let script = scripts.output_script();
5355
let address_format = AddressFormat::from_optional_str(address_format.as_deref())
5456
.map_err(|e| WasmMiniscriptError::new(&format!("Invalid address format: {}", e)))?;

packages/wasm-utxo/src/fixed_script_wallet/test_utils/mod.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use super::wallet_scripts::{Chain, WalletScripts};
77
use crate::bitcoin::bip32::{DerivationPath, Fingerprint, Xpriv, Xpub};
88
use crate::bitcoin::psbt::{Input as PsbtInput, Output as PsbtOutput, Psbt};
99
use crate::bitcoin::{Transaction, TxIn, TxOut};
10-
use crate::RootWalletKeys;
10+
use crate::{Network, RootWalletKeys};
1111
use std::collections::BTreeMap;
1212
use std::str::FromStr;
1313

@@ -32,8 +32,12 @@ pub fn get_test_wallet_keys(seed: &str) -> XpubTriple {
3232
/// Create a PSBT output for an external wallet (different keys)
3333
pub fn create_external_output(seed: &str) -> PsbtOutput {
3434
let xpubs = get_test_wallet_keys(seed);
35-
let _scripts =
36-
WalletScripts::from_wallet_keys(&RootWalletKeys::new(xpubs), Chain::P2wshExternal, 0);
35+
let _scripts = WalletScripts::from_wallet_keys(
36+
&RootWalletKeys::new(xpubs),
37+
Chain::P2wshExternal,
38+
0,
39+
&Network::Bitcoin.output_script_support(),
40+
).unwrap();
3741
PsbtOutput {
3842
bip32_derivation: BTreeMap::new(),
3943
// witness_script: scripts.witness_script,

packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/mod.rs

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ pub use checkmultisig::{
1212
pub use checksigverify::{build_p2tr_ns_script, ScriptP2tr};
1313
pub use singlesig::{build_p2pk_script, ScriptP2shP2pk};
1414

15+
use crate::address::networks::OutputScriptSupport;
1516
use crate::bitcoin::bip32::{ChildNumber, DerivationPath};
1617
use crate::bitcoin::ScriptBuf;
18+
use crate::error::WasmMiniscriptError;
1719
use crate::fixed_script_wallet::wallet_keys::{to_pub_triple, PubTriple, XpubTriple};
1820
use crate::RootWalletKeys;
1921
use std::convert::TryFrom;
@@ -51,32 +53,41 @@ impl std::fmt::Display for WalletScripts {
5153
}
5254

5355
impl WalletScripts {
54-
pub fn new(keys: &PubTriple, chain: Chain) -> WalletScripts {
56+
pub fn new(
57+
keys: &PubTriple,
58+
chain: Chain,
59+
script_support: &OutputScriptSupport,
60+
) -> Result<WalletScripts, WasmMiniscriptError> {
5561
match chain {
5662
Chain::P2shExternal | Chain::P2shInternal => {
63+
script_support.assert_legacy()?;
5764
let script = build_multisig_script_2_of_3(keys);
58-
WalletScripts::P2sh(ScriptP2sh {
65+
Ok(WalletScripts::P2sh(ScriptP2sh {
5966
redeem_script: script,
60-
})
67+
}))
6168
}
6269
Chain::P2shP2wshExternal | Chain::P2shP2wshInternal => {
70+
script_support.assert_segwit()?;
6371
let script = build_multisig_script_2_of_3(keys);
64-
WalletScripts::P2shP2wsh(ScriptP2shP2wsh {
72+
Ok(WalletScripts::P2shP2wsh(ScriptP2shP2wsh {
6573
redeem_script: script.clone().to_p2wsh(),
6674
witness_script: script,
67-
})
75+
}))
6876
}
6977
Chain::P2wshExternal | Chain::P2wshInternal => {
78+
script_support.assert_segwit()?;
7079
let script = build_multisig_script_2_of_3(keys);
71-
WalletScripts::P2wsh(ScriptP2wsh {
80+
Ok(WalletScripts::P2wsh(ScriptP2wsh {
7281
witness_script: script,
73-
})
82+
}))
7483
}
7584
Chain::P2trInternal | Chain::P2trExternal => {
76-
WalletScripts::P2trLegacy(ScriptP2tr::new(keys, false))
85+
script_support.assert_taproot()?;
86+
Ok(WalletScripts::P2trLegacy(ScriptP2tr::new(keys, false)))
7787
}
7888
Chain::P2trMusig2Internal | Chain::P2trMusig2External => {
79-
WalletScripts::P2trMusig2(ScriptP2tr::new(keys, true))
89+
script_support.assert_taproot()?;
90+
Ok(WalletScripts::P2trMusig2(ScriptP2tr::new(keys, true)))
8091
}
8192
}
8293
}
@@ -85,11 +96,12 @@ impl WalletScripts {
8596
wallet_keys: &RootWalletKeys,
8697
chain: Chain,
8798
index: u32,
88-
) -> WalletScripts {
99+
script_support: &OutputScriptSupport,
100+
) -> Result<WalletScripts, WasmMiniscriptError> {
89101
let derived_keys = wallet_keys
90102
.derive_for_chain_and_index(chain as u32, index)
91103
.unwrap();
92-
WalletScripts::new(&to_pub_triple(&derived_keys), chain)
104+
WalletScripts::new(&to_pub_triple(&derived_keys), chain, script_support)
93105
}
94106

95107
pub fn output_script(&self) -> ScriptBuf {
@@ -199,9 +211,16 @@ mod tests {
199211
use super::*;
200212
use crate::fixed_script_wallet::test_utils::fixtures;
201213
use crate::fixed_script_wallet::wallet_keys::tests::get_test_wallet_keys;
214+
use crate::Network;
202215

203216
fn assert_output_script(keys: &RootWalletKeys, chain: Chain, expected_script: &str) {
204-
let scripts = WalletScripts::from_wallet_keys(keys, chain, 0);
217+
let scripts = WalletScripts::from_wallet_keys(
218+
keys,
219+
chain,
220+
0,
221+
&Network::Bitcoin.output_script_support(),
222+
)
223+
.unwrap();
205224
let output_script = scripts.output_script();
206225
assert_eq!(output_script.to_hex_string(), expected_script);
207226
}
@@ -382,7 +401,13 @@ mod tests {
382401

383402
let (chain, index) =
384403
parse_fixture_paths(input_fixture).expect("Failed to parse fixture paths");
385-
let scripts = WalletScripts::from_wallet_keys(&wallet_keys, chain, index);
404+
let scripts = WalletScripts::from_wallet_keys(
405+
&wallet_keys,
406+
chain,
407+
index,
408+
&Network::Bitcoin.output_script_support(),
409+
)
410+
.expect("Failed to create wallet scripts");
386411

387412
// Use the new helper methods for validation
388413
match (scripts, input_fixture) {

0 commit comments

Comments
 (0)