@@ -124,8 +124,19 @@ pub const ZTXID_OUTPUTS_PERSONAL: &[u8; 16] = b"ZTxIdOutputsHash";
124124/// ZIP-244 personalization for the sapling digest.
125125pub const ZTXID_SAPLING_PERSONAL : & [ u8 ; 16 ] = b"ZTxIdSaplingHash" ;
126126/// ZIP-244 outer txid personalization prefix; the 4-byte consensus branch id
127- /// (little-endian) is appended to form the full 16-byte personalization.
127+ /// (little-endian) is appended to form the full 16-byte personalization. Shared by the
128+ /// txid digest and the signature (SIGHASH) digest.
128129pub const ZCASH_TXID_PERSONAL_PREFIX : & [ u8 ; 12 ] = b"ZcashTxHash_" ;
130+ /// ZIP-244 §S.2b personalization for the transparent input-amounts sig sub-digest.
131+ pub const ZTXID_AMOUNTS_SIG_PERSONAL : & [ u8 ; 16 ] = b"ZTxTrAmountsHash" ;
132+ /// ZIP-244 §S.2c personalization for the transparent scriptPubKeys sig sub-digest.
133+ pub const ZTXID_SCRIPTS_SIG_PERSONAL : & [ u8 ; 16 ] = b"ZTxTrScriptsHash" ;
134+ /// ZIP-244 §S.2g personalization for the per-input (txin) sig sub-digest. Hashed over the
135+ /// empty string for a shielded signature; over the signed input's fields for a transparent one.
136+ pub const ZTXID_TXIN_SIG_PERSONAL : & [ u8 ; 16 ] = b"Zcash___TxInHash" ;
137+ /// The `SIGHASH_ALL` hash type byte. Shielded signatures always use `SIGHASH_ALL`, and the
138+ /// BitGo transparent flows sign with `SIGHASH_ALL` as well.
139+ const SIGHASH_ALL : u8 = 0x01 ;
129140
130141/// Boundary between the compact note plaintext and the memo inside `encCiphertext`.
131142const ENC_COMPACT_END : usize = 52 ;
@@ -388,6 +399,169 @@ pub fn compute_v6_txid_from_bytes(bytes: &[u8]) -> Result<[u8; 32], ZcashV6Error
388399 Ok ( compute_v6_txid ( & tx) )
389400}
390401
402+ /// ZIP-244 §S.2 `transparent_sig_digest`, parameterized by the per-input (`txin`) sub-digest.
403+ ///
404+ /// The shielded and per-input transparent signature digests differ *only* in the `txin`
405+ /// component: empty for a shielded signature, populated for the transparent input being signed.
406+ /// Every other sub-digest (prevouts / amounts / scriptPubKeys / sequences / outputs) is identical
407+ /// under `SIGHASH_ALL`, so both callers share this builder. `input_amounts` and
408+ /// `input_script_pubkeys` are the spent outputs' values and scriptPubKeys, one per transparent
409+ /// input in input order.
410+ fn transparent_sig_digest_with_txin (
411+ tx : & ZcashV6Transaction ,
412+ input_amounts : & [ i64 ] ,
413+ input_script_pubkeys : & [ miniscript:: bitcoin:: ScriptBuf ] ,
414+ txin_sig_hash : [ u8 ; 32 ] ,
415+ ) -> [ u8 ; 32 ] {
416+ let inputs = & tx. transparent . input ;
417+ // Per ZIP-244, the sig transparent digest collapses to the txid one only when there are
418+ // neither transparent inputs nor outputs (e.g. a fully-shielded tx). With outputs but no
419+ // inputs (unshield: shielded spend -> transparent output), the full S.2 structure still
420+ // applies, since hash_type/amounts/scripts/txin differ from the txid digest whenever
421+ // outputs is non-empty.
422+ if inputs. is_empty ( ) && tx. transparent . output . is_empty ( ) {
423+ return transparent_txid_digest ( tx) ;
424+ }
425+
426+ debug_assert_eq ! (
427+ input_amounts. len( ) ,
428+ inputs. len( ) ,
429+ "input_amounts must have one entry per transparent input"
430+ ) ;
431+ debug_assert_eq ! (
432+ input_script_pubkeys. len( ) ,
433+ inputs. len( ) ,
434+ "input_script_pubkeys must have one entry per transparent input"
435+ ) ;
436+
437+ // S.2a prevouts / S.2d sequences / S.2e outputs: identical to the txid sub-digests under
438+ // SIGHASH_ALL (plain concatenation, no CompactSize count).
439+ let mut prevouts = Vec :: new ( ) ;
440+ let mut sequences = Vec :: new ( ) ;
441+ for txin in inputs {
442+ txin. previous_output
443+ . consensus_encode ( & mut prevouts)
444+ . expect ( "vec write is infallible" ) ;
445+ txin. sequence
446+ . consensus_encode ( & mut sequences)
447+ . expect ( "vec write is infallible" ) ;
448+ }
449+ let mut outputs_data = Vec :: new ( ) ;
450+ for txout in & tx. transparent . output {
451+ txout
452+ . consensus_encode ( & mut outputs_data)
453+ . expect ( "vec write is infallible" ) ;
454+ }
455+ let prevouts_hash = blake2b_256_personal ( & prevouts, ZTXID_PREVOUTS_PERSONAL ) ;
456+ let sequence_hash = blake2b_256_personal ( & sequences, ZTXID_SEQUENCE_PERSONAL ) ;
457+ let outputs_hash = blake2b_256_personal ( & outputs_data, ZTXID_OUTPUTS_PERSONAL ) ;
458+
459+ // S.2b amounts / S.2c scriptPubKeys: `zcash_encoding::Array` encoding — each element written
460+ // back-to-back with NO outer count. Amounts are 8-byte signed LE; scriptPubKeys use their
461+ // standard (individually length-prefixed) consensus encoding.
462+ let mut amounts_data = Vec :: with_capacity ( input_amounts. len ( ) * 8 ) ;
463+ for amount in input_amounts {
464+ amounts_data. extend_from_slice ( & amount. to_le_bytes ( ) ) ;
465+ }
466+ let mut scripts_data = Vec :: new ( ) ;
467+ for script in input_script_pubkeys {
468+ script
469+ . consensus_encode ( & mut scripts_data)
470+ . expect ( "vec write is infallible" ) ;
471+ }
472+ let amounts_hash = blake2b_256_personal ( & amounts_data, ZTXID_AMOUNTS_SIG_PERSONAL ) ;
473+ let scripts_hash = blake2b_256_personal ( & scripts_data, ZTXID_SCRIPTS_SIG_PERSONAL ) ;
474+
475+ let mut data = Vec :: with_capacity ( 1 + 32 * 6 ) ;
476+ data. push ( SIGHASH_ALL ) ;
477+ data. extend_from_slice ( & prevouts_hash) ;
478+ data. extend_from_slice ( & amounts_hash) ;
479+ data. extend_from_slice ( & scripts_hash) ;
480+ data. extend_from_slice ( & sequence_hash) ;
481+ data. extend_from_slice ( & outputs_hash) ;
482+ data. extend_from_slice ( & txin_sig_hash) ;
483+ blake2b_256_personal ( & data, ZTXID_TRANSPARENT_PERSONAL )
484+ }
485+
486+ /// Combine the five ZIP-244 component digests into the outer signature (SIGHASH) digest, using
487+ /// the same `ZcashTxHash_`+branch-id personalization as the txid.
488+ fn v6_sig_digest_from_transparent ( tx : & ZcashV6Transaction , transparent : [ u8 ; 32 ] ) -> [ u8 ; 32 ] {
489+ let header = header_digest ( tx) ;
490+ let sapling = blake2b_256_personal ( & [ ] , ZTXID_SAPLING_PERSONAL ) ;
491+ let orchard = orchard_v6_empty_digest ( ) ;
492+ let ironwood = ironwood_digest ( tx. ironwood_bundle . as_ref ( ) ) ;
493+
494+ let mut data = Vec :: with_capacity ( 32 * 5 ) ;
495+ data. extend_from_slice ( & header) ;
496+ data. extend_from_slice ( & transparent) ;
497+ data. extend_from_slice ( & sapling) ;
498+ data. extend_from_slice ( & orchard) ;
499+ data. extend_from_slice ( & ironwood) ;
500+
501+ let mut personal = [ 0u8 ; 16 ] ;
502+ personal[ ..12 ] . copy_from_slice ( ZCASH_TXID_PERSONAL_PREFIX ) ;
503+ personal[ 12 ..] . copy_from_slice ( & tx. consensus_branch_id . to_le_bytes ( ) ) ;
504+ blake2b_256_personal ( & data, & personal)
505+ }
506+
507+ /// ZIP-244 v6 **shielded** signature hash (SIGHASH_ALL) — the message the Ironwood binding
508+ /// signature (and any shielded spend-auth signature) signs.
509+ ///
510+ /// `input_amounts` / `input_script_pubkeys` describe the spent outputs, one per transparent input
511+ /// in input order. Result is a 32-byte digest in internal order (not a txid; not reversed).
512+ pub fn compute_v6_sig_digest (
513+ tx : & ZcashV6Transaction ,
514+ input_amounts : & [ i64 ] ,
515+ input_script_pubkeys : & [ miniscript:: bitcoin:: ScriptBuf ] ,
516+ ) -> [ u8 ; 32 ] {
517+ // Shielded signature: the per-input (txin) sub-digest is the empty personalized hash.
518+ let txin_sig_hash = blake2b_256_personal ( & [ ] , ZTXID_TXIN_SIG_PERSONAL ) ;
519+ let transparent =
520+ transparent_sig_digest_with_txin ( tx, input_amounts, input_script_pubkeys, txin_sig_hash) ;
521+ v6_sig_digest_from_transparent ( tx, transparent)
522+ }
523+
524+ /// ZIP-244 v6 **transparent** per-input signature hash (SIGHASH_ALL) — the message the key
525+ /// controlling transparent input `input_index` signs.
526+ ///
527+ /// `script_code` is the script being signed for that input (its prevout scriptPubKey for P2PKH,
528+ /// or the redeem/witness script for P2SH/P2WSH). `input_amounts` / `input_script_pubkeys` are the
529+ /// spent outputs' values and scriptPubKeys for every input, in input order.
530+ pub fn compute_v6_transparent_sighash (
531+ tx : & ZcashV6Transaction ,
532+ input_index : usize ,
533+ script_code : & miniscript:: bitcoin:: Script ,
534+ input_amounts : & [ i64 ] ,
535+ input_script_pubkeys : & [ miniscript:: bitcoin:: ScriptBuf ] ,
536+ ) -> Result < [ u8 ; 32 ] , ZcashV6Error > {
537+ let txin = tx
538+ . transparent
539+ . input
540+ . get ( input_index)
541+ . ok_or ( ZcashV6Error :: UnexpectedEof ) ?;
542+ let amount = * input_amounts
543+ . get ( input_index)
544+ . ok_or ( ZcashV6Error :: UnexpectedEof ) ?;
545+
546+ // S.2g: prevout ‖ value(8, signed LE) ‖ scriptCode(length-prefixed) ‖ nSequence(4, LE).
547+ let mut txin_data = Vec :: new ( ) ;
548+ txin. previous_output
549+ . consensus_encode ( & mut txin_data)
550+ . expect ( "vec write is infallible" ) ;
551+ txin_data. extend_from_slice ( & amount. to_le_bytes ( ) ) ;
552+ script_code
553+ . consensus_encode ( & mut txin_data)
554+ . expect ( "vec write is infallible" ) ;
555+ txin. sequence
556+ . consensus_encode ( & mut txin_data)
557+ . expect ( "vec write is infallible" ) ;
558+ let txin_sig_hash = blake2b_256_personal ( & txin_data, ZTXID_TXIN_SIG_PERSONAL ) ;
559+
560+ let transparent =
561+ transparent_sig_digest_with_txin ( tx, input_amounts, input_script_pubkeys, txin_sig_hash) ;
562+ Ok ( v6_sig_digest_from_transparent ( tx, transparent) )
563+ }
564+
391565/// A fully parsed Zcash v6 (Ironwood) transaction.
392566#[ derive( Debug , Clone , PartialEq , Eq ) ]
393567pub struct ZcashV6Transaction {
@@ -809,6 +983,190 @@ mod tests {
809983 }
810984 }
811985
986+ fn sample_tx_with_inputs ( ) -> ZcashV6Transaction {
987+ ZcashV6Transaction {
988+ version_group_id : ZCASH_IRONWOOD_VERSION_GROUP_ID ,
989+ consensus_branch_id : 0x37a5165b ,
990+ transparent : sample_transparent ( true ) ,
991+ expiry_height : 0 ,
992+ sapling_value_balance : 0 ,
993+ ironwood_bundle : Some ( sample_bundle ( ) ) ,
994+ }
995+ }
996+
997+ // ---- v6 signature-digest regression tests ----
998+ // (The full independent oracle — zebra recomputing these digests and verifying the binding
999+ // signature over them — runs in the ironwood_build build→prove→combine test, where a real
1000+ // bundle with known input amounts/scripts exists.)
1001+
1002+ #[ test]
1003+ fn sig_digest_is_deterministic ( ) {
1004+ let tx = sample_tx_with_inputs ( ) ;
1005+ let amounts = [ 12_345i64 ] ;
1006+ let scripts = [ ScriptBuf :: from ( vec ! [ 0x76u8 , 0xa9 , 0x14 ] ) ] ;
1007+ assert_eq ! (
1008+ compute_v6_sig_digest( & tx, & amounts, & scripts) ,
1009+ compute_v6_sig_digest( & tx, & amounts, & scripts)
1010+ ) ;
1011+ }
1012+
1013+ #[ test]
1014+ fn shielded_sig_digest_commits_to_amounts_and_scripts ( ) {
1015+ let tx = sample_tx_with_inputs ( ) ;
1016+ let base = compute_v6_sig_digest ( & tx, & [ 12_345 ] , & [ ScriptBuf :: from ( vec ! [ 0x76u8 , 0xa9 ] ) ] ) ;
1017+ // Differs from the txid (which does not commit to input amounts/scripts).
1018+ assert_ne ! ( base, compute_v6_txid( & tx) ) ;
1019+ assert_ne ! (
1020+ base,
1021+ compute_v6_sig_digest( & tx, & [ 99_999 ] , & [ ScriptBuf :: from( vec![ 0x76u8 , 0xa9 ] ) ] )
1022+ ) ;
1023+ assert_ne ! (
1024+ base,
1025+ compute_v6_sig_digest( & tx, & [ 12_345 ] , & [ ScriptBuf :: from( vec![ 0x51u8 ] ) ] )
1026+ ) ;
1027+ }
1028+
1029+ #[ test]
1030+ fn shielded_sig_digest_without_inputs_equals_txid ( ) {
1031+ let tx = ZcashV6Transaction {
1032+ version_group_id : ZCASH_IRONWOOD_VERSION_GROUP_ID ,
1033+ consensus_branch_id : 0x37a5165b ,
1034+ transparent : sample_transparent ( false ) ,
1035+ expiry_height : 0 ,
1036+ sapling_value_balance : 0 ,
1037+ ironwood_bundle : Some ( sample_bundle ( ) ) ,
1038+ } ;
1039+ assert_eq ! ( compute_v6_sig_digest( & tx, & [ ] , & [ ] ) , compute_v6_txid( & tx) ) ;
1040+ }
1041+
1042+ #[ test]
1043+ fn shielded_sig_digest_unshield_case_differs_from_txid ( ) {
1044+ // Unshield: transparent output present, no transparent inputs. The sig transparent
1045+ // digest must NOT collapse to the txid digest here, since the txid digest omits
1046+ // hash_type/amounts/scripts/txin while the sig digest includes them.
1047+ let ( _, output) = {
1048+ let sample = sample_transparent ( true ) ;
1049+ ( sample. input , sample. output )
1050+ } ;
1051+ let transparent = Transaction {
1052+ version : miniscript:: bitcoin:: transaction:: Version :: non_standard ( 6 ) ,
1053+ input : vec ! [ ] ,
1054+ output,
1055+ lock_time : miniscript:: bitcoin:: locktime:: absolute:: LockTime :: from_consensus ( 0 ) ,
1056+ } ;
1057+ let tx = ZcashV6Transaction {
1058+ version_group_id : ZCASH_IRONWOOD_VERSION_GROUP_ID ,
1059+ consensus_branch_id : 0x37a5165b ,
1060+ transparent,
1061+ expiry_height : 0 ,
1062+ sapling_value_balance : 0 ,
1063+ ironwood_bundle : Some ( sample_bundle ( ) ) ,
1064+ } ;
1065+ assert_ne ! ( compute_v6_sig_digest( & tx, & [ ] , & [ ] ) , compute_v6_txid( & tx) ) ;
1066+ }
1067+
1068+ #[ test]
1069+ fn transparent_sighash_differs_from_shielded_and_varies_by_input ( ) {
1070+ let tx = sample_tx_with_inputs ( ) ;
1071+ let amounts = [ 12_345i64 ] ;
1072+ let scripts = [ ScriptBuf :: from ( vec ! [ 0x76u8 , 0xa9 , 0x14 ] ) ] ;
1073+ let script_code = ScriptBuf :: from ( vec ! [ 0x76u8 , 0xa9 , 0x14 , 0x88 , 0xac ] ) ;
1074+
1075+ let shielded = compute_v6_sig_digest ( & tx, & amounts, & scripts) ;
1076+ let transparent =
1077+ compute_v6_transparent_sighash ( & tx, 0 , script_code. as_script ( ) , & amounts, & scripts)
1078+ . unwrap ( ) ;
1079+ // The per-input transparent sighash populates the txin component, so it must differ from
1080+ // the shielded (empty-txin) digest over the same tx.
1081+ assert_ne ! ( shielded, transparent) ;
1082+ // Deterministic.
1083+ assert_eq ! (
1084+ transparent,
1085+ compute_v6_transparent_sighash( & tx, 0 , script_code. as_script( ) , & amounts, & scripts)
1086+ . unwrap( )
1087+ ) ;
1088+ // A different script_code changes the digest.
1089+ let other_code = ScriptBuf :: from ( vec ! [ 0x51u8 ] ) ;
1090+ assert_ne ! (
1091+ transparent,
1092+ compute_v6_transparent_sighash( & tx, 0 , other_code. as_script( ) , & amounts, & scripts)
1093+ . unwrap( )
1094+ ) ;
1095+ // Out-of-range input index is an error, not a panic.
1096+ assert ! ( compute_v6_transparent_sighash(
1097+ & tx,
1098+ 9 ,
1099+ script_code. as_script( ) ,
1100+ & amounts,
1101+ & scripts
1102+ )
1103+ . is_err( ) ) ;
1104+ }
1105+
1106+ /// Golden oracle for [`compute_v6_transparent_sighash`]: verify the **real** ECDSA signature
1107+ /// carried in a signed transparent→Ironwood testnet tx against the sighash we compute.
1108+ ///
1109+ /// The tx is the `v6_shield1zec` fixture (one P2PKH transparent input → 1 ZEC Ironwood note).
1110+ /// The spent output's value and scriptPubKey are not on the wire but are committed by ZIP-244;
1111+ /// they come from the sandbox reference that produced this tx (prevout
1112+ /// `058886a9…:0`, 313_990_000 zat, standard P2PKH). If the transaction's own signature verifies
1113+ /// against our digest, the digest is byte-correct — an independent check, since the signature
1114+ /// was produced by an external signer, not by this code.
1115+ #[ test]
1116+ fn golden_transparent_sighash_verifies_real_signature ( ) {
1117+ use miniscript:: bitcoin:: script:: Instruction ;
1118+ use miniscript:: bitcoin:: secp256k1:: { ecdsa:: Signature , Message , PublicKey , Secp256k1 } ;
1119+
1120+ let raw = hex:: decode ( load_zcash_fixture ( "v6_shield1zec_rawtx.hex" ) . trim ( ) ) . unwrap ( ) ;
1121+ let tx = decode_v6_transaction ( & raw ) . unwrap ( ) ;
1122+ assert_eq ! ( tx. transparent. input. len( ) , 1 ) ;
1123+ assert_eq ! ( tx. transparent. output. len( ) , 1 ) ;
1124+
1125+ // Spent output (from the reference that built this tx); ZIP-244 commits to both.
1126+ let prevout_value: i64 = 313_990_000 ;
1127+ let prevout_script = ScriptBuf :: from (
1128+ hex:: decode ( "76a9147c6b843a25873c036aff575516e3802bcc47f63488ac" ) . unwrap ( ) ,
1129+ ) ;
1130+
1131+ // P2PKH scriptSig = <DER sig ‖ SIGHASH_ALL> <pubkey>. The scriptCode signed for a P2PKH
1132+ // input is its scriptPubKey.
1133+ let pushes: Vec < Vec < u8 > > = tx. transparent . input [ 0 ]
1134+ . script_sig
1135+ . instructions ( )
1136+ . map ( |i| i. expect ( "valid scriptSig" ) )
1137+ . filter_map ( |i| match i {
1138+ Instruction :: PushBytes ( pb) => Some ( pb. as_bytes ( ) . to_vec ( ) ) ,
1139+ Instruction :: Op ( _) => None ,
1140+ } )
1141+ . collect ( ) ;
1142+ assert_eq ! ( pushes. len( ) , 2 , "P2PKH scriptSig has sig + pubkey" ) ;
1143+ let sig_bytes = & pushes[ 0 ] ;
1144+ let pubkey_bytes = & pushes[ 1 ] ;
1145+ assert_eq ! (
1146+ * sig_bytes. last( ) . unwrap( ) ,
1147+ SIGHASH_ALL ,
1148+ "signature uses SIGHASH_ALL"
1149+ ) ;
1150+ let der = & sig_bytes[ ..sig_bytes. len ( ) - 1 ] ;
1151+
1152+ let sighash = compute_v6_transparent_sighash (
1153+ & tx,
1154+ 0 ,
1155+ prevout_script. as_script ( ) ,
1156+ & [ prevout_value] ,
1157+ std:: slice:: from_ref ( & prevout_script) ,
1158+ )
1159+ . unwrap ( ) ;
1160+
1161+ let secp = Secp256k1 :: verification_only ( ) ;
1162+ let msg = Message :: from_digest ( sighash) ;
1163+ let mut sig = Signature :: from_der ( der) . expect ( "DER signature" ) ;
1164+ sig. normalize_s ( ) ;
1165+ let pk = PublicKey :: from_slice ( pubkey_bytes) . expect ( "valid pubkey" ) ;
1166+ secp. verify_ecdsa ( & msg, & sig, & pk)
1167+ . expect ( "the tx's real signature verifies against compute_v6_transparent_sighash" ) ;
1168+ }
1169+
8121170 #[ test]
8131171 fn digest_is_deterministic ( ) {
8141172 let b = sample_bundle ( ) ;
0 commit comments