@@ -11,6 +11,11 @@ use std::{
1111
1212use chrono:: { DateTime , Utc } ;
1313
14+ use zcash_script:: {
15+ opcode:: PossiblyBad ,
16+ script:: { self , Evaluable as _} ,
17+ solver, Opcode ,
18+ } ;
1419use zebra_chain:: {
1520 amount:: { Amount , NegativeAllowed , NonNegative } ,
1621 block:: Height ,
@@ -630,6 +635,220 @@ pub fn tx_transparent_coinbase_spends_maturity(
630635 Ok ( ( ) )
631636}
632637
638+ /// The maximum number of signature operations in the redeem script of a standard P2SH input.
639+ ///
640+ /// This is zcashd's `MAX_P2SH_SIGOPS` standardness (policy) constant:
641+ /// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.h#L20>
642+ pub const MAX_P2SH_SIGOPS : u32 = 15 ;
643+
644+ /// The maximum size in bytes of the scriptSig of a standard transaction input.
645+ ///
646+ /// This is zcashd's `MAX_STANDARD_SCRIPTSIG_SIZE` standardness (policy) constant:
647+ /// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L92-L99>
648+ pub const MAX_STANDARD_SCRIPTSIG_SIZE : usize = 1650 ;
649+
650+ /// Classify a script using the `zcash_script` solver.
651+ ///
652+ /// Returns `Some(kind)` for standard script types, `None` for non-standard.
653+ ///
654+ /// Mirrors the classification done by zcashd's `Solver()`.
655+ pub fn standard_script_kind ( lock_script : & transparent:: Script ) -> Option < solver:: ScriptKind > {
656+ let code = script:: Code ( lock_script. as_raw_bytes ( ) . to_vec ( ) ) ;
657+ let component = code. to_component ( ) . ok ( ) ?. refine ( ) . ok ( ) ?;
658+ solver:: standard ( & component)
659+ }
660+
661+ /// Returns the expected number of scriptSig arguments for a given script kind.
662+ ///
663+ /// Mirrors zcashd's `ScriptSigArgsExpected()`:
664+ /// <https://github.com/zcash/zcash/blob/v6.11.0/src/script/standard.cpp#L135>
665+ ///
666+ /// Returns `None` for non-standard types (TX_NONSTANDARD, TX_NULL_DATA).
667+ pub ( super ) fn script_sig_args_expected ( kind : & solver:: ScriptKind ) -> Option < usize > {
668+ match kind {
669+ solver:: ScriptKind :: PubKey { .. } => Some ( 1 ) ,
670+ solver:: ScriptKind :: PubKeyHash { .. } => Some ( 2 ) ,
671+ solver:: ScriptKind :: ScriptHash { .. } => Some ( 1 ) ,
672+ solver:: ScriptKind :: MultiSig { required, .. } => Some ( * required as usize + 1 ) ,
673+ solver:: ScriptKind :: NullData { .. } => None ,
674+ }
675+ }
676+
677+ /// Extract the redeemed script bytes from a P2SH scriptSig.
678+ ///
679+ /// The redeemed script is the last data push in the scriptSig.
680+ /// Returns `None` if the scriptSig has no push operations.
681+ ///
682+ /// # Precondition
683+ ///
684+ /// The scriptSig should be push-only (enforced by [`mempool_standard_input_scripts`] before this
685+ /// function is reached). Non-push opcodes are silently ignored.
686+ pub ( super ) fn extract_p2sh_redeemed_script ( unlock_script : & transparent:: Script ) -> Option < Vec < u8 > > {
687+ let code = script:: Code ( unlock_script. as_raw_bytes ( ) . to_vec ( ) ) ;
688+ let mut last_push_data: Option < Vec < u8 > > = None ;
689+ for opcode in code. parse ( ) . flatten ( ) {
690+ if let PossiblyBad :: Good ( Opcode :: PushValue ( pv) ) = opcode {
691+ last_push_data = Some ( pv. value ( ) ) ;
692+ }
693+ }
694+ last_push_data
695+ }
696+
697+ /// Count the number of push operations in a script.
698+ ///
699+ /// For a push-only script (already enforced for mempool scriptSigs),
700+ /// this equals the stack depth after evaluation.
701+ pub ( super ) fn count_script_push_ops ( script_bytes : & [ u8 ] ) -> usize {
702+ let code = script:: Code ( script_bytes. to_vec ( ) ) ;
703+ code. parse ( )
704+ . filter ( |op| matches ! ( op, Ok ( PossiblyBad :: Good ( Opcode :: PushValue ( _) ) ) ) )
705+ . count ( )
706+ }
707+
708+ /// Returns `true` if all of a transaction's transparent inputs are standard.
709+ ///
710+ /// Mirrors zcashd's `AreInputsStandard()`:
711+ /// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L136>
712+ ///
713+ /// For each input:
714+ /// 1. The spent output's scriptPubKey must be a known standard type (via the `zcash_script`
715+ /// solver). Non-standard scripts and OP_RETURN outputs are rejected.
716+ /// 2. The scriptSig stack depth must match `ScriptSigArgsExpected()`.
717+ /// 3. For P2SH inputs:
718+ /// - If the redeemed script is standard, its expected args are added to the total.
719+ /// - If the redeemed script is non-standard, it must have at most [`MAX_P2SH_SIGOPS`] sigops.
720+ ///
721+ /// # Correctness
722+ ///
723+ /// Callers must ensure `spent_outputs.len()` matches the number of transparent inputs.
724+ /// If the lengths differ, `false` is returned.
725+ pub fn are_inputs_standard ( tx : & Transaction , spent_outputs : & [ transparent:: Output ] ) -> bool {
726+ if tx. inputs ( ) . len ( ) != spent_outputs. len ( ) {
727+ return false ;
728+ }
729+ for ( input, spent_output) in tx. inputs ( ) . iter ( ) . zip ( spent_outputs. iter ( ) ) {
730+ let unlock_script = match input {
731+ transparent:: Input :: PrevOut { unlock_script, .. } => unlock_script,
732+ transparent:: Input :: Coinbase { .. } => continue ,
733+ } ;
734+
735+ // Step 1: Classify the spent output's scriptPubKey via the zcash_script solver.
736+ let script_kind = match standard_script_kind ( & spent_output. lock_script ) {
737+ Some ( kind) => kind,
738+ None => return false ,
739+ } ;
740+
741+ // Step 2: Get expected number of scriptSig arguments.
742+ // Returns None for TX_NONSTANDARD and TX_NULL_DATA.
743+ let mut n_args_expected = match script_sig_args_expected ( & script_kind) {
744+ Some ( n) => n,
745+ None => return false ,
746+ } ;
747+
748+ // Step 3: Count actual push operations in scriptSig.
749+ // For push-only scripts (enforced before this function), this equals the stack depth.
750+ let stack_size = count_script_push_ops ( unlock_script. as_raw_bytes ( ) ) ;
751+
752+ // Step 4: P2SH-specific checks.
753+ if matches ! ( script_kind, solver:: ScriptKind :: ScriptHash { .. } ) {
754+ let Some ( redeemed_bytes) = extract_p2sh_redeemed_script ( unlock_script) else {
755+ return false ;
756+ } ;
757+
758+ let redeemed_code = script:: Code ( redeemed_bytes) ;
759+
760+ // Classify the redeemed script using the zcash_script solver.
761+ let redeemed_kind = {
762+ let component = redeemed_code
763+ . to_component ( )
764+ . ok ( )
765+ . and_then ( |c| c. refine ( ) . ok ( ) ) ;
766+ component. and_then ( |c| solver:: standard ( & c) )
767+ } ;
768+
769+ match redeemed_kind {
770+ Some ( ref inner_kind) => {
771+ // Standard redeemed script: add its expected args.
772+ match script_sig_args_expected ( inner_kind) {
773+ Some ( inner) => n_args_expected += inner,
774+ None => return false ,
775+ }
776+ }
777+ None => {
778+ // Non-standard redeemed script: accept if sigops <= limit.
779+ // Matches zcashd: "Any other Script with less than 15 sigops OK:
780+ // ... extra data left on the stack after execution is OK, too"
781+ let sigops = redeemed_code. sig_op_count ( true ) ;
782+ if sigops > MAX_P2SH_SIGOPS {
783+ return false ;
784+ }
785+
786+ // This input is acceptable; move on to the next input.
787+ continue ;
788+ }
789+ }
790+ }
791+
792+ // Step 5: Reject if scriptSig has wrong number of stack items.
793+ if stack_size != n_args_expected {
794+ return false ;
795+ }
796+ }
797+ true
798+ }
799+
800+ /// Standardness (policy) checks on a mempool transaction's transparent input scripts, applied
801+ /// *before* the transaction is dispatched to script verification. The goal is to avoid the
802+ /// expensive verification for non-standard transactions which would be rejected anyway
803+ /// by `Storage::reject_if_non_standard_tx()`; this is a subset of the checks
804+ /// in that function.
805+ ///
806+ /// `spent_outputs` must contain the output spent by each of the transaction's transparent inputs,
807+ /// in input order.
808+ ///
809+ /// # Correctness
810+ ///
811+ /// `spent_outputs.len()` must equal the number of transparent inputs in `tx`: if the lengths
812+ /// differ, `zip()` silently truncates, and some inputs are not checked.
813+ pub fn mempool_standard_input_scripts (
814+ tx : & Transaction ,
815+ spent_outputs : & [ transparent:: Output ] ,
816+ ) -> Result < ( ) , TransactionError > {
817+ if tx. inputs ( ) . len ( ) != spent_outputs. len ( ) {
818+ return Err ( TransactionError :: Other ( format ! (
819+ "spent_outputs must align with transaction inputs for non-coinbase txs: inputs={}, spent_outputs={}" ,
820+ tx. inputs( ) . len( ) ,
821+ spent_outputs. len( ) ,
822+ ) ) ) ;
823+ }
824+
825+ for ( input_index, input) in tx. inputs ( ) . iter ( ) . enumerate ( ) {
826+ let unlock_script = match input {
827+ transparent:: Input :: PrevOut { unlock_script, .. } => unlock_script,
828+ transparent:: Input :: Coinbase { .. } => continue ,
829+ } ;
830+
831+ // Rule: the scriptSig must be within the standard size limit.
832+ let size = unlock_script. as_raw_bytes ( ) . len ( ) ;
833+ if size > MAX_STANDARD_SCRIPTSIG_SIZE {
834+ return Err ( TransactionError :: NonStandardScriptSigSize { input_index, size } ) ;
835+ }
836+
837+ // Rule: the scriptSig must be push-only.
838+ if !script:: Code ( unlock_script. as_raw_bytes ( ) . to_vec ( ) ) . is_push_only ( ) {
839+ return Err ( TransactionError :: NonStandardScriptSigNotPushOnly { input_index } ) ;
840+ }
841+ }
842+
843+ // Rule: all transparent inputs must pass `AreInputsStandard()` checks:
844+ // https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L137
845+ if !are_inputs_standard ( tx, spent_outputs) {
846+ return Err ( TransactionError :: NonStandardInputs ) ;
847+ }
848+
849+ Ok ( ( ) )
850+ }
851+
633852/// Checks the `nConsensusBranchId` field.
634853///
635854/// # Consensus
0 commit comments