@@ -28,6 +28,15 @@ use zcash_protocol::memo::MemoBytes;
2828use zcash_protocol:: value:: Zatoshis ;
2929use zcash_transparent:: builder:: TransparentSigningSet ;
3030
31+ // PCZT / FROST signing imports
32+ use blake2b_simd:: Hash as Blake2bHash ;
33+ // PcztParts/PcztResult are accessed via TxBuilder::build_for_pczt() return type.
34+ use zcash_primitives:: transaction:: sighash:: SignableInput ;
35+ use zcash_primitives:: transaction:: sighash_v5:: v5_signature_hash;
36+ use zcash_primitives:: transaction:: txid:: TxIdDigester ;
37+ use zcash_primitives:: transaction:: { Authorization , TransactionData , TxDigests , TxVersion } ;
38+ use zcash_protocol:: value:: ZatBalance ;
39+
3140use crate :: config:: Config ;
3241use crate :: db:: Db ;
3342use crate :: frost_signer:: { FrostSigner , SigningMode } ;
@@ -93,6 +102,16 @@ impl sapling_crypto::prover::OutputProver for NoopOutputProver {
93102 }
94103}
95104
105+ /// Authorization type for PCZT sighash computation.
106+ /// Combines EffectsOnly from all shielded protocols.
107+ struct PcztEffectsOnly ;
108+
109+ impl Authorization for PcztEffectsOnly {
110+ type TransparentAuth = zcash_transparent:: bundle:: EffectsOnly ;
111+ type SaplingAuth = sapling_crypto:: bundle:: EffectsOnly ;
112+ type OrchardAuth = orchard:: bundle:: EffectsOnly ;
113+ }
114+
96115const MAX_CHECKPOINTS : usize = 100 ;
97116
98117type OrchardShardStore = MemoryShardStore < MerkleHashOrchard , BlockHeight > ;
@@ -541,14 +560,20 @@ impl AnchorWallet {
541560 }
542561
543562 // Build, prove, and sign
544- let rng = rand_core:: OsRng ;
545- let transparent_signing = TransparentSigningSet :: new ( ) ;
546563 let fee_rule = FeeRule :: standard ( ) ;
547564
548- // Both signing modes use the single-key path for now.
549- // FROST threshold signing produces a standalone authorization
550- // signature over the sighash. Full FROST-in-bundle signing
551- // requires the PCZT flow (orchard::pczt) to access alpha.
565+ // FROST threshold mode: use PCZT flow to access alpha for
566+ // rerandomized spend authorization signatures.
567+ if self . signing_mode == SigningMode :: FrostThreshold {
568+ if let Some ( ref frost) = self . frost_signer {
569+ return self
570+ . build_anchor_tx_frost ( builder, frost, params, config, & fee_rule, position) ;
571+ }
572+ }
573+
574+ // Single-key path: standard build with SpendAuthorizingKey.
575+ let rng = rand_core:: OsRng ;
576+ let transparent_signing = TransparentSigningSet :: new ( ) ;
552577 let sak = SpendAuthorizingKey :: from ( & self . sk ) ;
553578 let result = builder
554579 . build (
@@ -562,24 +587,6 @@ impl AnchorWallet {
562587 )
563588 . map_err ( |e| anyhow:: anyhow!( "Transaction build failed: {:?}" , e) ) ?;
564589
565- // If FROST mode, also produce a threshold signature as proof of
566- // multi-party authorization. This is logged and can be verified
567- // independently against the FROST group public key.
568- if self . signing_mode == SigningMode :: FrostThreshold {
569- if let Some ( ref frost) = self . frost_signer {
570- let sighash = result. transaction ( ) . txid ( ) . as_ref ( ) . to_vec ( ) ;
571- match frost. sign_raw ( & sighash) {
572- Ok ( sig) => {
573- let sig_hex = hex:: encode ( <[ u8 ; 64 ] >:: from ( sig) ) ;
574- tracing:: info!( "FROST threshold signature: {}" , & sig_hex[ ..32 ] , ) ;
575- }
576- Err ( e) => {
577- tracing:: error!( "FROST signing failed: {}" , e) ;
578- }
579- }
580- }
581- }
582-
583590 let tx = result. transaction ( ) ;
584591 let txid = tx. txid ( ) . to_string ( ) ;
585592
@@ -598,6 +605,152 @@ impl AnchorWallet {
598605 Ok ( ( tx_hex, txid, position) )
599606 }
600607
608+ /// Build an anchor transaction using FROST threshold signing via the PCZT flow.
609+ ///
610+ /// PCZT (Partially Created Zcash Transaction) pipeline:
611+ /// 1. builder.build_for_pczt() -> PcztParts with orchard::pczt::Bundle
612+ /// 2. extract_effects() -> Bundle<EffectsOnly> for sighash computation
613+ /// 3. v5_signature_hash() -> shielded sighash
614+ /// 4. finalize_io(sighash) -> computes bsk, signs dummy spends
615+ /// 5. For each action: read alpha, FROST sign(sighash, alpha), apply_signature()
616+ /// 6. create_proof(ProvingKey) -> Orchard halo2 proof
617+ /// 7. extract() -> Bundle<Unbound>
618+ /// 8. apply_binding_signature(sighash) -> Bundle<Authorized>
619+ /// 9. TransactionData::from_parts() + freeze() -> Transaction
620+ #[ allow( clippy:: too_many_arguments) ]
621+ fn build_anchor_tx_frost < P : Parameters > (
622+ & self ,
623+ builder : TxBuilder < ' _ , P , ( ) > ,
624+ frost : & FrostSigner ,
625+ params : & P ,
626+ config : & Config ,
627+ fee_rule : & FeeRule ,
628+ position : Position ,
629+ ) -> Result < ( String , String , Position ) > {
630+ let mut rng = rand_core:: OsRng ;
631+
632+ // Step 1: Build PCZT parts (consumes the builder).
633+ let pczt_result = builder
634+ . build_for_pczt ( & mut rng, fee_rule)
635+ . map_err ( |e| anyhow:: anyhow!( "PCZT build failed: {:?}" , e) ) ?;
636+
637+ let pczt_parts = pczt_result. pczt_parts ;
638+ let mut orchard_pczt = pczt_parts
639+ . orchard
640+ . ok_or_else ( || anyhow:: anyhow!( "No Orchard bundle in PCZT" ) ) ?;
641+
642+ // Step 2: Compute sighash from transaction effects.
643+ let orchard_effects: Option < orchard:: Bundle < orchard:: bundle:: EffectsOnly , ZatBalance > > =
644+ orchard_pczt
645+ . extract_effects ( )
646+ . map_err ( |e| anyhow:: anyhow!( "extract_effects: {:?}" , e) ) ?;
647+
648+ let tx_version =
649+ TxVersion :: suggested_for_branch ( BranchId :: for_height ( params, pczt_parts. expiry_height ) ) ;
650+ let consensus_branch_id = BranchId :: for_height ( params, pczt_parts. expiry_height ) ;
651+
652+ let tx_data: TransactionData < PcztEffectsOnly > = TransactionData :: from_parts (
653+ tx_version,
654+ consensus_branch_id,
655+ 0 , // lock_time
656+ pczt_parts. expiry_height ,
657+ None , // transparent
658+ None , // sprout
659+ None , // sapling
660+ orchard_effects,
661+ ) ;
662+
663+ let txid_parts: TxDigests < Blake2bHash > = tx_data. digest ( TxIdDigester ) ;
664+ let shielded_sighash: [ u8 ; 32 ] =
665+ v5_signature_hash ( & tx_data, & SignableInput :: Shielded , & txid_parts)
666+ . as_ref ( )
667+ . try_into ( )
668+ . expect ( "sighash is 32 bytes" ) ;
669+
670+ tracing:: info!( "PCZT sighash: {}" , & hex:: encode( & shielded_sighash) [ ..16 ] ) ;
671+
672+ // Step 3: Finalize IO (computes bsk, signs dummy spends).
673+ orchard_pczt
674+ . finalize_io ( shielded_sighash, & mut rng)
675+ . map_err ( |e| anyhow:: anyhow!( "finalize_io: {:?}" , e) ) ?;
676+
677+ // Step 4: FROST sign each non-dummy action.
678+ for ( i, action) in orchard_pczt. actions_mut ( ) . iter_mut ( ) . enumerate ( ) {
679+ // Skip actions that already have signatures (dummy spends).
680+ if action. spend ( ) . spend_auth_sig ( ) . is_some ( ) {
681+ continue ;
682+ }
683+
684+ let alpha = * action
685+ . spend ( )
686+ . alpha ( )
687+ . as_ref ( )
688+ . ok_or_else ( || anyhow:: anyhow!( "action {} missing alpha" , i) ) ?;
689+
690+ // FROST sign with rerandomization: sign(sighash, alpha)
691+ let frost_sig = frost. sign ( & shielded_sighash, alpha) ?;
692+
693+ // Convert reddsa::Signature to orchard's internal Signature type.
694+ let sig_bytes: [ u8 ; 64 ] = frost_sig. into ( ) ;
695+ let orchard_sig = orchard:: primitives:: redpallas:: Signature :: <
696+ orchard:: primitives:: redpallas:: SpendAuth ,
697+ > :: from ( sig_bytes) ;
698+
699+ action
700+ . apply_signature ( shielded_sighash, orchard_sig)
701+ . map_err ( |e| anyhow:: anyhow!( "apply_signature action {}: {:?}" , i, e) ) ?;
702+
703+ tracing:: info!( "FROST signed action {}" , i) ;
704+ }
705+
706+ // Step 5: Create Orchard proof.
707+ let proving_key = orchard:: circuit:: ProvingKey :: build ( ) ;
708+ orchard_pczt
709+ . create_proof ( & proving_key, & mut rng)
710+ . map_err ( |e| anyhow:: anyhow!( "create_proof: {:?}" , e) ) ?;
711+
712+ // Step 6: Extract authorized bundle.
713+ let unbound_bundle: orchard:: Bundle < orchard:: pczt:: Unbound , ZatBalance > = orchard_pczt
714+ . extract ( )
715+ . map_err ( |e| anyhow:: anyhow!( "extract: {:?}" , e) ) ?
716+ . ok_or_else ( || anyhow:: anyhow!( "empty orchard bundle after extract" ) ) ?;
717+
718+ let authorized_bundle = unbound_bundle
719+ . apply_binding_signature ( shielded_sighash, & mut rng)
720+ . ok_or_else ( || anyhow:: anyhow!( "binding signature verification failed" ) ) ?;
721+
722+ // Step 7: Assemble final Transaction.
723+ let authorized_tx: TransactionData < zcash_primitives:: transaction:: Authorized > =
724+ TransactionData :: from_parts (
725+ tx_version,
726+ consensus_branch_id,
727+ 0 , // lock_time
728+ pczt_parts. expiry_height ,
729+ None , // transparent
730+ None , // sprout
731+ None , // sapling
732+ Some ( authorized_bundle) ,
733+ ) ;
734+
735+ let tx = authorized_tx
736+ . freeze ( )
737+ . map_err ( |e| anyhow:: anyhow!( "freeze: {:?}" , e) ) ?;
738+
739+ let txid = tx. txid ( ) . to_string ( ) ;
740+ let mut raw = Vec :: new ( ) ;
741+ tx. write ( & mut raw)
742+ . map_err ( |e| anyhow:: anyhow!( "serialize: {:?}" , e) ) ?;
743+ let tx_hex = hex:: encode ( & raw ) ;
744+
745+ tracing:: info!(
746+ "FROST anchor tx built: txid={} ({} zat output)" ,
747+ txid. get( ..16 ) . unwrap_or( & txid) ,
748+ config. anchor_amount_zat,
749+ ) ;
750+
751+ Ok ( ( tx_hex, txid, position) )
752+ }
753+
601754 /// Build a signed payout transaction to an external Orchard address.
602755 /// Returns (raw_tx_hex, txid_hex, spent_position).
603756 pub fn build_payout_tx < P : Parameters > (
0 commit comments