33use core:: fmt:: { self , Display } ;
44use core:: str:: FromStr ;
55
6+ use bitcoin:: hashes:: { hash160, ripemd160, sha256} ;
7+
68use super :: key:: XKeyParseError ;
79use super :: { DerivPaths , DescriptorKeyParseError , Wildcard } ;
8- use crate :: { BTreeSet , Descriptor , DescriptorPublicKey , String , Translator , Vec } ;
10+ use crate :: { BTreeSet , Descriptor , DescriptorPublicKey , String , ToString , Translator , Vec } ;
911
1012mod key_expression;
1113
@@ -61,7 +63,30 @@ impl Translator<KeyExpression> for WalletPolicyTranslator {
6163 . ok_or ( WalletPolicyError :: KeyInfoInvalidKeyIndex ( idx) )
6264 }
6365
64- translate_hash_fail ! ( KeyExpression , DescriptorPublicKey , Self :: Error ) ;
66+ // Hash terminals: KeyExpression stores hashes as hex `String` (for
67+ // template round-tripping), DescriptorPublicKey uses the concrete
68+ // `bitcoin::hashes::*::Hash` types. Parse the hex string into the
69+ // binary form during materialization.
70+
71+ fn sha256 ( & mut self , s : & String ) -> Result < sha256:: Hash , Self :: Error > {
72+ s. parse :: < sha256:: Hash > ( )
73+ . map_err ( |_| WalletPolicyError :: TranslatorInvalidHashHex ( "sha256" , s. clone ( ) ) )
74+ }
75+
76+ fn hash256 ( & mut self , s : & String ) -> Result < crate :: hash256:: Hash , Self :: Error > {
77+ s. parse :: < crate :: hash256:: Hash > ( )
78+ . map_err ( |_| WalletPolicyError :: TranslatorInvalidHashHex ( "hash256" , s. clone ( ) ) )
79+ }
80+
81+ fn ripemd160 ( & mut self , s : & String ) -> Result < ripemd160:: Hash , Self :: Error > {
82+ s. parse :: < ripemd160:: Hash > ( )
83+ . map_err ( |_| WalletPolicyError :: TranslatorInvalidHashHex ( "ripemd160" , s. clone ( ) ) )
84+ }
85+
86+ fn hash160 ( & mut self , s : & String ) -> Result < hash160:: Hash , Self :: Error > {
87+ s. parse :: < hash160:: Hash > ( )
88+ . map_err ( |_| WalletPolicyError :: TranslatorInvalidHashHex ( "hash160" , s. clone ( ) ) )
89+ }
6590}
6691
6792impl Translator < DescriptorPublicKey > for WalletPolicyTranslator {
@@ -81,7 +106,22 @@ impl Translator<DescriptorPublicKey> for WalletPolicyTranslator {
81106 Ok ( ke)
82107 }
83108
84- translate_hash_fail ! ( DescriptorPublicKey , KeyExpression , Self :: Error ) ;
109+ // Hash terminals: DescriptorPublicKey uses concrete Hash types,
110+ // KeyExpression stores them as hex `String`. Render to lowercase hex
111+ // (the `Display` impl on `bitcoin::hashes::*::Hash`) so the resulting
112+ // template prints hashes in their canonical form.
113+
114+ fn sha256 ( & mut self , h : & sha256:: Hash ) -> Result < String , Self :: Error > { Ok ( h. to_string ( ) ) }
115+
116+ fn hash256 ( & mut self , h : & crate :: hash256:: Hash ) -> Result < String , Self :: Error > {
117+ Ok ( h. to_string ( ) )
118+ }
119+
120+ fn ripemd160 ( & mut self , h : & ripemd160:: Hash ) -> Result < String , Self :: Error > {
121+ Ok ( h. to_string ( ) )
122+ }
123+
124+ fn hash160 ( & mut self , h : & hash160:: Hash ) -> Result < String , Self :: Error > { Ok ( h. to_string ( ) ) }
85125}
86126
87127impl WalletPolicy {
@@ -208,6 +248,8 @@ pub enum WalletPolicyError {
208248 WalletPolicyParseFromString ( String ) ,
209249 /// Couldn't set key info on WalletPolicy
210250 WalletPolicyInvalidKeyInfo ,
251+ /// Hash terminal in template had invalid hex (kind, raw input)
252+ TranslatorInvalidHashHex ( & ' static str , String ) ,
211253}
212254
213255impl From < WalletPolicyError > for DescriptorKeyParseError {
@@ -258,6 +300,9 @@ impl Display for WalletPolicyError {
258300 WalletPolicyError :: WalletPolicyInvalidKeyInfo => {
259301 write ! ( f, "Invalid key information for WalletPolicy template" )
260302 }
303+ WalletPolicyError :: TranslatorInvalidHashHex ( kind, raw) => {
304+ write ! ( f, "Invalid hex for {kind} hash terminal: {raw}" )
305+ }
261306 }
262307 }
263308}
@@ -366,6 +411,68 @@ mod tests {
366411 }
367412 }
368413
414+ // Hash-terminal round-trip tests. Before this fix the translator used
415+ // `translate_hash_fail!` and `into_descriptor` returned an error on any
416+ // hash terminal; BIP 388 places no restriction on them.
417+
418+ // BIP 84 mainnet origin; same xpub as VALID_TEMPLATES.
419+ const TEST_XPUB : & str = "[6738736c/84'/0'/0']xpub6CRQzb8u9dmMcq5XAwwRn9gcoYCjndJkhKgD11WKzbVGd932UmrExWFxCAvRnDN3ez6ZujLmMvmLBaSWdfWVn75L83Qxu1qSX4fJNrJg2Gt/<0;1>/*" ;
420+
421+ // (kind, lowercase-hex of correct byte length).
422+ const HASH_VECTORS : & [ ( & str , & str ) ] = & [
423+ ( "sha256" , "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" ) ,
424+ ( "hash256" , "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" ) ,
425+ ( "ripemd160" , "1234567890abcdef1234567890abcdef12345678" ) ,
426+ ( "hash160" , "fedcba0987654321fedcba0987654321fedcba09" ) ,
427+ ] ;
428+
429+ #[ test]
430+ fn hash_terminals_round_trip ( ) {
431+ for & ( kind, hex) in HASH_VECTORS {
432+ let s = format ! ( "wsh(and_v(v:pk({TEST_XPUB}),{kind}({hex})))" ) ;
433+ let descriptor = Descriptor :: < DescriptorPublicKey > :: from_str ( & s) . unwrap ( ) ;
434+ let policy = WalletPolicy :: from_str ( & s) . unwrap ( ) ;
435+ assert_eq ! ( policy. into_descriptor( ) . unwrap( ) , descriptor) ;
436+ }
437+ }
438+
439+ #[ test]
440+ fn mixed_hash_terminals_round_trip ( ) {
441+ // All distinct pairs of hash kinds in one policy. Catches cross-type
442+ // dispatch errors, including byte-order quirks specific to hash256.
443+ for ( i, & ( a_kind, a_hex) ) in HASH_VECTORS . iter ( ) . enumerate ( ) {
444+ for & ( b_kind, b_hex) in HASH_VECTORS . iter ( ) . skip ( i + 1 ) {
445+ let s = format ! (
446+ "wsh(and_v(v:and_v(v:pk({TEST_XPUB}),{a_kind}({a_hex})),{b_kind}({b_hex})))"
447+ ) ;
448+ let descriptor = Descriptor :: < DescriptorPublicKey > :: from_str ( & s) . unwrap ( ) ;
449+ let policy = WalletPolicy :: from_str ( & s) . unwrap ( ) ;
450+ assert_eq ! ( policy. into_descriptor( ) . unwrap( ) , descriptor) ;
451+ }
452+ }
453+ }
454+
455+ // The public parser validates hex length up-front, so invalid hex
456+ // never reaches the translator through `from_str`. This drives the
457+ // translator directly to pin the defensive `TranslatorInvalidHashHex`
458+ // variant.
459+ #[ test]
460+ fn translator_invalid_hash_hex_errors ( ) {
461+ let mut t = WalletPolicyTranslator { key_info : Vec :: new ( ) } ;
462+ let bad = String :: from ( "not_hex" ) ;
463+
464+ macro_rules! assert_bad {
465+ ( $method: ident, $kind: literal) => { {
466+ let err = Translator :: <KeyExpression >:: $method( & mut t, & bad) . unwrap_err( ) ;
467+ assert!( matches!( err, WalletPolicyError :: TranslatorInvalidHashHex ( $kind, _) ) ) ;
468+ } } ;
469+ }
470+ assert_bad ! ( sha256, "sha256" ) ;
471+ assert_bad ! ( hash256, "hash256" ) ;
472+ assert_bad ! ( ripemd160, "ripemd160" ) ;
473+ assert_bad ! ( hash160, "hash160" ) ;
474+ }
475+
369476 #[ test]
370477 fn can_set_key_info ( ) {
371478 let mut template_only =
0 commit comments