55
66use miniscript:: bitcoin:: consensus:: { Decodable , Encodable } ;
77use miniscript:: bitcoin:: psbt:: Psbt ;
8- use miniscript:: bitcoin:: { Transaction , TxIn , TxOut , VarInt } ;
8+ use miniscript:: bitcoin:: { Transaction , VarInt } ;
99use std:: io:: Read ;
1010
11- /// Zcash Sapling version group ID
12- pub const ZCASH_SAPLING_VERSION_GROUP_ID : u32 = 0x892F2085 ;
13-
14- /// Zcash transaction metadata extracted from transaction bytes
15- ///
16- /// This struct provides the Zcash-specific fields without requiring
17- /// the full transaction to be stored.
18- #[ derive( Debug , Clone ) ]
19- pub struct ZcashTransactionMeta {
20- /// Number of inputs
21- pub input_count : usize ,
22- /// Number of outputs
23- pub output_count : usize ,
24- /// Zcash-specific: Version group ID for overwintered transactions
25- pub version_group_id : Option < u32 > ,
26- /// Zcash-specific: Expiry height
27- pub expiry_height : Option < u32 > ,
28- /// Whether this is a Zcash overwintered transaction
29- pub is_overwintered : bool ,
30- }
31-
32- /// Decode Zcash transaction metadata from bytes
33- ///
34- /// Extracts input/output counts and Zcash-specific fields (version_group_id, expiry_height)
35- /// from a Zcash overwintered transaction.
36- pub fn decode_zcash_transaction_meta ( bytes : & [ u8 ] ) -> Result < ZcashTransactionMeta , String > {
37- let mut slice = bytes;
38-
39- // Read version
40- let version = u32:: consensus_decode ( & mut slice)
41- . map_err ( |e| format ! ( "Failed to decode version: {}" , e) ) ?;
42-
43- let is_overwintered = ( version & 0x80000000 ) != 0 ;
44-
45- let version_group_id = if is_overwintered {
46- Some (
47- u32:: consensus_decode ( & mut slice)
48- . map_err ( |e| format ! ( "Failed to decode version group ID: {}" , e) ) ?,
49- )
50- } else {
51- None
52- } ;
53-
54- // Read inputs
55- let inputs: Vec < TxIn > =
56- Vec :: consensus_decode ( & mut slice) . map_err ( |e| format ! ( "Failed to decode inputs: {}" , e) ) ?;
57-
58- // Read outputs
59- let outputs: Vec < TxOut > = Vec :: consensus_decode ( & mut slice)
60- . map_err ( |e| format ! ( "Failed to decode outputs: {}" , e) ) ?;
61-
62- // Read lock_time
63- let _lock_time =
64- miniscript:: bitcoin:: locktime:: absolute:: LockTime :: consensus_decode ( & mut slice)
65- . map_err ( |e| format ! ( "Failed to decode lock_time: {}" , e) ) ?;
66-
67- // Read expiry height if overwintered
68- let expiry_height = if is_overwintered {
69- Some (
70- u32:: consensus_decode ( & mut slice)
71- . map_err ( |e| format ! ( "Failed to decode expiry height: {}" , e) ) ?,
72- )
73- } else {
74- None
75- } ;
76-
77- Ok ( ZcashTransactionMeta {
78- input_count : inputs. len ( ) ,
79- output_count : outputs. len ( ) ,
80- version_group_id,
81- expiry_height,
82- is_overwintered,
83- } )
84- }
85-
86- /// Decoded Zcash transaction with extracted Zcash-specific fields (internal use)
87- #[ derive( Debug , Clone ) ]
88- struct DecodedZcashTransaction {
89- /// The transaction in Bitcoin-compatible format
90- transaction : Transaction ,
91- /// Zcash-specific: Version group ID for overwintered transactions
92- version_group_id : Option < u32 > ,
93- /// Zcash-specific: Expiry height
94- expiry_height : Option < u32 > ,
95- /// Zcash-specific: Additional Sapling fields (valueBalance, nShieldedSpend, nShieldedOutput, etc.)
96- /// These are preserved as-is to maintain exact serialization
97- sapling_fields : Vec < u8 > ,
98- }
11+ pub use crate :: zcash:: transaction:: {
12+ decode_zcash_transaction_meta, ZcashTransactionMeta , ZCASH_SAPLING_VERSION_GROUP_ID ,
13+ } ;
9914
10015/// A Zcash-compatible PSBT that can handle overwintered transactions
10116///
@@ -116,61 +31,6 @@ pub struct ZcashBitGoPsbt {
11631 pub sapling_fields : Vec < u8 > ,
11732}
11833
119- /// Decode a Zcash transaction from bytes, extracting Zcash-specific fields
120- fn decode_zcash_transaction (
121- bytes : & [ u8 ] ,
122- ) -> Result < DecodedZcashTransaction , super :: DeserializeError > {
123- let mut slice = bytes;
124-
125- // Read version
126- let version = u32:: consensus_decode ( & mut slice) ?;
127-
128- let is_overwintered = ( version & 0x80000000 ) != 0 ;
129-
130- let version_group_id = if is_overwintered {
131- Some ( u32:: consensus_decode ( & mut slice) ?)
132- } else {
133- None
134- } ;
135-
136- // Read inputs
137- let inputs: Vec < TxIn > = Vec :: consensus_decode ( & mut slice) ?;
138-
139- // Read outputs
140- let outputs: Vec < TxOut > = Vec :: consensus_decode ( & mut slice) ?;
141-
142- // Read lock_time
143- let lock_time =
144- miniscript:: bitcoin:: locktime:: absolute:: LockTime :: consensus_decode ( & mut slice) ?;
145-
146- // Read expiry height if overwintered
147- let expiry_height = if is_overwintered {
148- Some ( u32:: consensus_decode ( & mut slice) ?)
149- } else {
150- None
151- } ;
152-
153- // Capture any remaining bytes (Sapling fields: valueBalance, nShieldedSpend, nShieldedOutput, etc.)
154- let sapling_fields = slice. to_vec ( ) ;
155-
156- // Create transaction with standard version (without overwintered bit)
157- let transaction = Transaction {
158- version : miniscript:: bitcoin:: transaction:: Version :: non_standard (
159- ( version & 0x7FFFFFFF ) as i32 ,
160- ) ,
161- input : inputs,
162- output : outputs,
163- lock_time,
164- } ;
165-
166- Ok ( DecodedZcashTransaction {
167- transaction,
168- version_group_id,
169- expiry_height,
170- sapling_fields,
171- } )
172- }
173-
17434impl ZcashBitGoPsbt {
17535 /// Get the network this PSBT is for
17636 pub fn network ( & self ) -> crate :: Network {
@@ -182,52 +42,18 @@ impl ZcashBitGoPsbt {
18242 & self ,
18343 tx : & Transaction ,
18444 ) -> Result < Vec < u8 > , super :: DeserializeError > {
185- let mut tx_bytes = Vec :: new ( ) ;
186-
187- // Version with overwintered bit
188- let zcash_version = ( tx. version . 0 as u32 ) | 0x80000000 ;
189- zcash_version. consensus_encode ( & mut tx_bytes) . map_err ( |e| {
190- super :: DeserializeError :: Network ( format ! ( "Failed to encode Zcash version: {}" , e) )
191- } ) ?;
192-
193- // Version group ID
194- self . version_group_id
195- . unwrap_or ( ZCASH_SAPLING_VERSION_GROUP_ID )
196- . consensus_encode ( & mut tx_bytes)
197- . map_err ( |e| {
198- super :: DeserializeError :: Network ( format ! (
199- "Failed to encode version group ID: {}" ,
200- e
201- ) )
202- } ) ?;
203-
204- // Inputs
205- tx. input . consensus_encode ( & mut tx_bytes) . map_err ( |e| {
206- super :: DeserializeError :: Network ( format ! ( "Failed to encode inputs: {}" , e) )
207- } ) ?;
208-
209- // Outputs
210- tx. output . consensus_encode ( & mut tx_bytes) . map_err ( |e| {
211- super :: DeserializeError :: Network ( format ! ( "Failed to encode outputs: {}" , e) )
212- } ) ?;
213-
214- // Lock time
215- tx. lock_time . consensus_encode ( & mut tx_bytes) . map_err ( |e| {
216- super :: DeserializeError :: Network ( format ! ( "Failed to encode lock_time: {}" , e) )
217- } ) ?;
218-
219- // Expiry height
220- self . expiry_height
221- . unwrap_or ( 0 )
222- . consensus_encode ( & mut tx_bytes)
223- . map_err ( |e| {
224- super :: DeserializeError :: Network ( format ! ( "Failed to encode expiry height: {}" , e) )
225- } ) ?;
226-
227- // Sapling fields (valueBalance, nShieldedSpend, nShieldedOutput, etc.)
228- tx_bytes. extend_from_slice ( & self . sapling_fields ) ;
229-
230- Ok ( tx_bytes)
45+ let parts = crate :: zcash:: transaction:: ZcashTransactionParts {
46+ transaction : tx. clone ( ) ,
47+ is_overwintered : true ,
48+ version_group_id : Some (
49+ self . version_group_id
50+ . unwrap_or ( ZCASH_SAPLING_VERSION_GROUP_ID ) ,
51+ ) ,
52+ expiry_height : Some ( self . expiry_height . unwrap_or ( 0 ) ) ,
53+ sapling_fields : self . sapling_fields . clone ( ) ,
54+ } ;
55+ crate :: zcash:: transaction:: encode_zcash_transaction_parts ( & parts)
56+ . map_err ( super :: DeserializeError :: Network )
23157 }
23258
23359 /// Reconstruct the unsigned Zcash transaction bytes from the PSBT
@@ -330,14 +156,15 @@ impl ZcashBitGoPsbt {
330156 if !key_data. is_empty ( ) && key_data[ 0 ] == 0x00 && key_data. len ( ) == 1 {
331157 // This is the unsigned transaction
332158 found_tx = true ;
333- let decoded = decode_zcash_transaction ( & val_data) ?;
334- version_group_id = decoded. version_group_id ;
335- expiry_height = decoded. expiry_height ;
336- sapling_fields = decoded. sapling_fields ;
159+ let parts = crate :: zcash:: transaction:: decode_zcash_transaction_parts ( & val_data)
160+ . map_err ( super :: DeserializeError :: Network ) ?;
161+ version_group_id = parts. version_group_id ;
162+ expiry_height = parts. expiry_height ;
163+ sapling_fields = parts. sapling_fields ;
337164
338165 // Serialize the modified transaction
339166 let mut tx_bytes = Vec :: new ( ) ;
340- decoded
167+ parts
341168 . transaction
342169 . consensus_encode ( & mut tx_bytes)
343170 . map_err ( |e| {
@@ -577,17 +404,14 @@ mod tests {
577404 // Expiry height
578405 0u32 . consensus_encode ( & mut tx_bytes) . unwrap ( ) ;
579406
580- let decoded = decode_zcash_transaction ( & tx_bytes) . unwrap ( ) ;
407+ let parts = crate :: zcash :: transaction :: decode_zcash_transaction_parts ( & tx_bytes) . unwrap ( ) ;
581408
582- assert_eq ! (
583- decoded. version_group_id,
584- Some ( ZCASH_SAPLING_VERSION_GROUP_ID )
585- ) ;
586- assert_eq ! ( decoded. expiry_height, Some ( 0 ) ) ;
587- assert_eq ! ( decoded. transaction. input. len( ) , 0 ) ;
588- assert_eq ! ( decoded. transaction. output. len( ) , 0 ) ;
409+ assert_eq ! ( parts. version_group_id, Some ( ZCASH_SAPLING_VERSION_GROUP_ID ) ) ;
410+ assert_eq ! ( parts. expiry_height, Some ( 0 ) ) ;
411+ assert_eq ! ( parts. transaction. input. len( ) , 0 ) ;
412+ assert_eq ! ( parts. transaction. output. len( ) , 0 ) ;
589413 // Should be empty for this simple test tx
590- assert ! ( decoded . sapling_fields. is_empty( ) ) ;
414+ assert ! ( parts . sapling_fields. is_empty( ) ) ;
591415 }
592416
593417 #[ test]
0 commit comments