Skip to content

Commit 217fa9f

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 7e7d78c commit 217fa9f

File tree

7 files changed

+532
-26
lines changed

7 files changed

+532
-26
lines changed

packages/wasm-utxo/js/fixedScriptWallet.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ 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(
16+
keys: WalletKeys,
17+
chain: number,
18+
index: number,
19+
network: UtxolibNetwork,
20+
): Uint8Array {
21+
return FixedScriptWalletNamespace.output_script(keys, chain, index, network);
1722
}
1823

1924
/**

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

Lines changed: 325 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

@@ -476,4 +571,234 @@ mod tests {
476571
.to_string()
477572
.contains("Unknown address format"));
478573
}
574+
575+
#[test]
576+
fn test_output_script_support_assert_legacy() {
577+
// Legacy should always succeed regardless of support flags
578+
let support_none = OutputScriptSupport {
579+
segwit: false,
580+
taproot: false,
581+
};
582+
assert!(support_none.assert_legacy().is_ok());
583+
584+
let support_all = OutputScriptSupport {
585+
segwit: true,
586+
taproot: true,
587+
};
588+
assert!(support_all.assert_legacy().is_ok());
589+
}
590+
591+
#[test]
592+
fn test_output_script_support_assert_segwit() {
593+
// Should succeed when segwit is supported
594+
let support_segwit = OutputScriptSupport {
595+
segwit: true,
596+
taproot: false,
597+
};
598+
assert!(support_segwit.assert_segwit().is_ok());
599+
600+
// Should fail when segwit is not supported
601+
let no_support = OutputScriptSupport {
602+
segwit: false,
603+
taproot: false,
604+
};
605+
let result = no_support.assert_segwit();
606+
assert!(result.is_err());
607+
assert!(result
608+
.unwrap_err()
609+
.to_string()
610+
.contains("Network does not support segwit"));
611+
}
612+
613+
#[test]
614+
fn test_output_script_support_assert_taproot() {
615+
// Should succeed when taproot is supported
616+
let support_taproot = OutputScriptSupport {
617+
segwit: true,
618+
taproot: true,
619+
};
620+
assert!(support_taproot.assert_taproot().is_ok());
621+
622+
// Should fail when taproot is not supported
623+
let no_support = OutputScriptSupport {
624+
segwit: true,
625+
taproot: false,
626+
};
627+
let result = no_support.assert_taproot();
628+
assert!(result.is_err());
629+
assert!(result
630+
.unwrap_err()
631+
.to_string()
632+
.contains("Network does not support taproot"));
633+
}
634+
635+
#[test]
636+
fn test_output_script_support_assert_support_legacy() {
637+
// Test with legacy P2PKH script
638+
let hash = hex::decode("62e907b15cbf27d5425399ebf6f0fb50ebb88f18").unwrap();
639+
let pubkey_hash = PubkeyHash::from_byte_array(hash.try_into().unwrap());
640+
let p2pkh_script = ScriptBuf::new_p2pkh(&pubkey_hash);
641+
642+
// Legacy scripts should work even without segwit/taproot support
643+
let no_support = OutputScriptSupport {
644+
segwit: false,
645+
taproot: false,
646+
};
647+
assert!(no_support.assert_support(&p2pkh_script).is_ok());
648+
649+
// Test with legacy P2SH script
650+
let hash2 = hex::decode("89abcdef89abcdef89abcdef89abcdef89abcdef").unwrap();
651+
let script_hash = crate::bitcoin::ScriptHash::from_byte_array(hash2.try_into().unwrap());
652+
let p2sh_script = ScriptBuf::new_p2sh(&script_hash);
653+
assert!(no_support.assert_support(&p2sh_script).is_ok());
654+
}
655+
656+
#[test]
657+
fn test_output_script_support_assert_support_segwit() {
658+
// Test with P2WPKH script (witness v0)
659+
let hash = hex::decode("751e76e8199196d454941c45d1b3a323f1433bd6").unwrap();
660+
let wpkh = crate::bitcoin::WPubkeyHash::from_byte_array(hash.try_into().unwrap());
661+
let p2wpkh_script = ScriptBuf::new_p2wpkh(&wpkh);
662+
663+
// Should succeed with segwit support
664+
let support_segwit = OutputScriptSupport {
665+
segwit: true,
666+
taproot: false,
667+
};
668+
assert!(support_segwit.assert_support(&p2wpkh_script).is_ok());
669+
670+
// Should fail without segwit support
671+
let no_support = OutputScriptSupport {
672+
segwit: false,
673+
taproot: false,
674+
};
675+
let result = no_support.assert_support(&p2wpkh_script);
676+
assert!(result.is_err());
677+
assert!(result
678+
.unwrap_err()
679+
.to_string()
680+
.contains("Network does not support segwit"));
681+
682+
// Test with P2WSH script (witness v0)
683+
let wsh = crate::bitcoin::WScriptHash::from_byte_array(
684+
hex::decode("751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45")
685+
.unwrap()
686+
.try_into()
687+
.unwrap(),
688+
);
689+
let p2wsh_script = ScriptBuf::new_p2wsh(&wsh);
690+
assert!(support_segwit.assert_support(&p2wsh_script).is_ok());
691+
assert!(no_support.assert_support(&p2wsh_script).is_err());
692+
}
693+
694+
#[test]
695+
fn test_output_script_support_assert_support_taproot() {
696+
// Test with P2TR script (witness v1)
697+
use crate::bitcoin::secp256k1::{Secp256k1, XOnlyPublicKey};
698+
699+
let secp = Secp256k1::verification_only();
700+
// Use a fixed x-only public key for testing
701+
let xonly_pk = XOnlyPublicKey::from_slice(
702+
&hex::decode("cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115")
703+
.unwrap(),
704+
)
705+
.unwrap();
706+
let p2tr_script = ScriptBuf::new_p2tr(&secp, xonly_pk, None);
707+
708+
// Should succeed with taproot support
709+
let support_taproot = OutputScriptSupport {
710+
segwit: true,
711+
taproot: true,
712+
};
713+
assert!(support_taproot.assert_support(&p2tr_script).is_ok());
714+
715+
// Should fail without taproot support (but with segwit)
716+
let no_taproot = OutputScriptSupport {
717+
segwit: true,
718+
taproot: false,
719+
};
720+
let result = no_taproot.assert_support(&p2tr_script);
721+
assert!(result.is_err());
722+
assert!(result
723+
.unwrap_err()
724+
.to_string()
725+
.contains("Network does not support taproot"));
726+
727+
// Should also fail without segwit or taproot
728+
let no_support = OutputScriptSupport {
729+
segwit: false,
730+
taproot: false,
731+
};
732+
let result = no_support.assert_support(&p2tr_script);
733+
assert!(result.is_err());
734+
}
735+
736+
#[test]
737+
fn test_output_script_support_network_specific() {
738+
// Test Bitcoin - should support segwit and taproot
739+
let btc_support = Network::Bitcoin.output_script_support();
740+
assert!(btc_support.segwit);
741+
assert!(btc_support.taproot);
742+
743+
// Test Bitcoin testnet - should support segwit and taproot
744+
let tbtc_support = Network::BitcoinTestnet3.output_script_support();
745+
assert!(tbtc_support.segwit);
746+
assert!(tbtc_support.taproot);
747+
748+
// Test Litecoin - should support segwit but not taproot
749+
let ltc_support = Network::Litecoin.output_script_support();
750+
assert!(ltc_support.segwit);
751+
assert!(!ltc_support.taproot);
752+
753+
// Test Bitcoin Gold - should support segwit but not taproot
754+
let btg_support = Network::BitcoinGold.output_script_support();
755+
assert!(btg_support.segwit);
756+
assert!(!btg_support.taproot);
757+
758+
// Test Dogecoin - should not support segwit or taproot
759+
let doge_support = Network::Dogecoin.output_script_support();
760+
assert!(!doge_support.segwit);
761+
assert!(!doge_support.taproot);
762+
763+
// Test Bitcoin Cash - should not support segwit or taproot
764+
let bch_support = Network::BitcoinCash.output_script_support();
765+
assert!(!bch_support.segwit);
766+
assert!(!bch_support.taproot);
767+
}
768+
769+
#[test]
770+
fn test_get_encode_codec_enforces_script_support() {
771+
// Test that get_encode_codec enforces script support via assert_support
772+
773+
// P2WPKH on Bitcoin should work
774+
let hash = hex::decode("751e76e8199196d454941c45d1b3a323f1433bd6").unwrap();
775+
let wpkh = crate::bitcoin::WPubkeyHash::from_byte_array(hash.try_into().unwrap());
776+
let p2wpkh_script = ScriptBuf::new_p2wpkh(&wpkh);
777+
assert!(get_encode_codec(Network::Bitcoin, &p2wpkh_script, AddressFormat::Default).is_ok());
778+
779+
// P2WPKH on Bitcoin Cash should fail (no segwit support)
780+
let result = get_encode_codec(Network::BitcoinCash, &p2wpkh_script, AddressFormat::Default);
781+
assert!(result.is_err());
782+
if let Err(e) = result {
783+
assert!(e.to_string().contains("Network does not support segwit"));
784+
}
785+
786+
// P2TR on Bitcoin should work
787+
use crate::bitcoin::secp256k1::{Secp256k1, XOnlyPublicKey};
788+
let secp = Secp256k1::verification_only();
789+
let xonly_pk = XOnlyPublicKey::from_slice(
790+
&hex::decode("cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115")
791+
.unwrap(),
792+
)
793+
.unwrap();
794+
let p2tr_script = ScriptBuf::new_p2tr(&secp, xonly_pk, None);
795+
assert!(get_encode_codec(Network::Bitcoin, &p2tr_script, AddressFormat::Default).is_ok());
796+
797+
// P2TR on Litecoin should fail (no taproot support)
798+
let result = get_encode_codec(Network::Litecoin, &p2tr_script, AddressFormat::Default);
799+
assert!(result.is_err());
800+
if let Err(e) = result {
801+
assert!(e.to_string().contains("Network does not support taproot"));
802+
}
803+
}
479804
}

0 commit comments

Comments
 (0)