@@ -56,6 +56,38 @@ const COUNTER_NAMES: [&str; 24] = [
5656
5757// ── Entry point ────────────────────────────────────────────────────────────
5858
59+ fn record_to_bare_input (
60+ rec : & ParsedRecord ,
61+ ) -> Result < Option < libbitcoinkernel_sys:: btck_bare_input > > {
62+ // Outcome-2 records are pre-verification rejects and carry no sighash.
63+ // They must be skipped before any capacity-indexed public-key slice.
64+ if rec. outcome == 2 {
65+ return Ok ( None ) ;
66+ }
67+ // parse_record already enforces these bounds for non-exempt records,
68+ // but the helper keeps the slice contract self-contained.
69+ if rec. der_len > 72 {
70+ bail ! ( "record: der_len {} exceeds 72" , rec. der_len) ;
71+ }
72+ if rec. pubkey_len > 65 {
73+ bail ! ( "record: pubkey_len {} exceeds 65" , rec. pubkey_len) ;
74+ }
75+
76+ let mut input = libbitcoinkernel_sys:: btck_bare_input {
77+ sighash : [ 0u8 ; 32 ] ,
78+ der_sig : [ 0u8 ; 72 ] ,
79+ pubkey : [ 0u8 ; 65 ] ,
80+ der_len : rec. der_len ,
81+ pubkey_len : rec. pubkey_len ,
82+ expected : rec. outcome , // 1=true, 0=false
83+ pad : [ 0u8 ; 4 ] ,
84+ } ;
85+ input. sighash . copy_from_slice ( & rec. sighash ) ;
86+ input. der_sig [ ..rec. der_len as usize ] . copy_from_slice ( & rec. der_sig [ ..rec. der_len as usize ] ) ;
87+ input. pubkey [ ..rec. pubkey_len as usize ] . copy_from_slice ( & rec. pubkey [ ..rec. pubkey_len as usize ] ) ;
88+ Ok ( Some ( input) )
89+ }
90+
5991fn main ( ) -> Result < ( ) > {
6092 let args = Args :: parse ( std:: env:: args_os ( ) . skip ( 1 ) ) ?;
6193
@@ -70,24 +102,10 @@ fn main() -> Result<()> {
70102 let mut inputs: Vec < libbitcoinkernel_sys:: btck_bare_input > = Vec :: with_capacity ( records. len ( ) ) ;
71103 let mut rejected: u64 = 0 ;
72104 for rec in & records {
73- if rec. outcome == 2 {
74- rejected += 1 ;
75- continue ;
105+ match record_to_bare_input ( rec) ? {
106+ None => rejected += 1 ,
107+ Some ( input ) => inputs . push ( input ) ,
76108 }
77- let mut input = libbitcoinkernel_sys:: btck_bare_input {
78- sighash : [ 0u8 ; 32 ] ,
79- der_sig : [ 0u8 ; 72 ] ,
80- pubkey : [ 0u8 ; 65 ] ,
81- der_len : rec. der_len ,
82- pubkey_len : rec. pubkey_len ,
83- expected : rec. outcome , // 1=true, 0=false
84- pad : [ 0u8 ; 4 ] ,
85- } ;
86- input. sighash . copy_from_slice ( & rec. sighash ) ;
87- input. der_sig [ ..rec. der_len as usize ] . copy_from_slice ( & rec. der_sig [ ..rec. der_len as usize ] ) ;
88- input. pubkey [ ..rec. pubkey_len as usize ]
89- . copy_from_slice ( & rec. pubkey [ ..rec. pubkey_len as usize ] ) ;
90- inputs. push ( input) ;
91109 }
92110 let expected_true_count: u64 = inputs. iter ( ) . filter ( |inp| inp. expected == 1 ) . count ( ) as u64 ;
93111
@@ -351,17 +369,58 @@ struct ParsedRecord {
351369 pubkey_len : u8 ,
352370 outcome : u8 ,
353371}
372+
373+ /// Exact over-capacity ECDSA reject shape accepted by the bare-secp reader.
374+ /// Native reason-1 records may preserve the original (>=66) pubkey_len only
375+ /// for outcome 2 with a null payload and unchanged padding.
376+ fn is_exempt_over_capacity_ecdsa_reject ( buf : & [ u8 ; RECORD_SIZE ] ) -> bool {
377+ if !( 1 ..=4 ) . contains ( & buf[ 40 ] ) {
378+ return false ;
379+ } // op_kind
380+ if !( 0 ..=1 ) . contains ( & buf[ 41 ] ) {
381+ return false ;
382+ } // sig_version
383+ if buf[ 42 ] != 2 {
384+ return false ;
385+ } // outcome
386+ if buf[ 43 ] != 0 {
387+ return false ;
388+ } // der_len
389+ if buf[ 45 ] != 0 {
390+ return false ;
391+ } // sighash_type
392+ if buf[ 46 ] != 1 {
393+ return false ;
394+ } // reject_reason
395+ if buf[ 47 ] != 0 {
396+ return false ;
397+ } // _pad0
398+ if buf[ 48 ..80 ] != [ 0 ; 32 ] {
399+ return false ;
400+ } // sighash
401+ if buf[ 80 ..152 ] != [ 0 ; 72 ] {
402+ return false ;
403+ } // der_sig
404+ if buf[ 152 ..217 ] != [ 0 ; 65 ] {
405+ return false ;
406+ } // pubkey
407+ if buf[ 217 ..224 ] != [ 0 ; 7 ] {
408+ return false ;
409+ } // _pad1
410+ true
411+ }
412+
354413fn parse_record ( buf : & [ u8 ; RECORD_SIZE ] , index : u64 ) -> Result < ParsedRecord > {
355414 let outcome = buf[ 42 ] ;
356415 let der_len = buf[ 43 ] ;
357416 let pubkey_len = buf[ 44 ] ;
358417
418+ if pubkey_len > 65 && !is_exempt_over_capacity_ecdsa_reject ( buf) {
419+ bail ! ( "record {index}: pubkey_len {pubkey_len} exceeds 65" ) ;
420+ }
359421 if der_len > 72 {
360422 bail ! ( "record {index}: der_len {der_len} exceeds 72" ) ;
361423 }
362- if pubkey_len > 65 {
363- bail ! ( "record {index}: pubkey_len {pubkey_len} exceeds 65" ) ;
364- }
365424 if outcome > 2 {
366425 bail ! ( "record {index}: outcome {outcome} exceeds 2" ) ;
367426 }
@@ -548,4 +607,128 @@ mod tests {
548607 } ;
549608 assert_eq ! ( rc, -1 ) ;
550609 }
610+
611+ fn preserved_over_capacity_row ( ) -> [ u8 ; RECORD_SIZE ] {
612+ let mut buf = [ 0u8 ; RECORD_SIZE ] ;
613+ let prefix: [ u8 ; 48 ] = [
614+ 0xcf , 0x42 , 0xbd , 0x87 , 0xb9 , 0x98 , 0x25 , 0x95 , 0xbf , 0x2d , 0x35 , 0x4c , 0x5f , 0x75 ,
615+ 0x8c , 0x14 , 0x4d , 0x66 , 0x33 , 0x01 , 0xce , 0xfc , 0x3b , 0x31 , 0xad , 0x31 , 0x32 , 0xf8 ,
616+ 0x7f , 0x49 , 0x18 , 0xd1 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x03 , 0x00 ,
617+ 0x02 , 0x00 , 0x42 , 0x00 , 0x01 , 0x00 ,
618+ ] ;
619+ buf[ ..48 ] . copy_from_slice ( & prefix) ;
620+ buf
621+ }
622+
623+ #[ test]
624+ fn parse_accepts_preserved_over_capacity_ecdsa_reject ( ) {
625+ let buf = preserved_over_capacity_row ( ) ;
626+ let rec = parse_record ( & buf, 0 ) . unwrap ( ) ;
627+ assert_eq ! ( rec. outcome, 2 ) ;
628+ assert_eq ! ( rec. der_len, 0 ) ;
629+ assert_eq ! ( rec. pubkey_len, 66 ) ;
630+ assert_eq ! ( rec. sighash, [ 0 ; 32 ] ) ;
631+ assert_eq ! ( rec. der_sig, [ 0 ; 72 ] ) ;
632+ assert_eq ! ( rec. pubkey, [ 0 ; 65 ] ) ;
633+ }
634+
635+ #[ test]
636+ fn parse_rejects_over_capacity_near_misses ( ) {
637+ let base = preserved_over_capacity_row ( ) ;
638+ let mutations: [ ( usize , u8 , & str ) ; 19 ] = [
639+ ( 40 , 0 , "op_kind=0" ) ,
640+ ( 40 , 5 , "op_kind=5" ) ,
641+ ( 41 , 2 , "sig_version=2" ) ,
642+ ( 41 , 3 , "sig_version=3" ) ,
643+ ( 42 , 0 , "outcome=0" ) ,
644+ ( 42 , 1 , "outcome=1" ) ,
645+ ( 43 , 1 , "der_len=1" ) ,
646+ ( 43 , 73 , "der_len=73" ) ,
647+ ( 45 , 1 , "sighash_type=1" ) ,
648+ ( 46 , 0 , "reject_reason=0" ) ,
649+ ( 46 , 2 , "reject_reason=2" ) ,
650+ ( 46 , 3 , "reject_reason=3" ) ,
651+ ( 46 , 4 , "reject_reason=4" ) ,
652+ ( 46 , 5 , "reject_reason=5" ) ,
653+ ( 46 , 6 , "reject_reason=6" ) ,
654+ ( 46 , 7 , "reject_reason=7" ) ,
655+ ( 46 , 8 , "reject_reason=8" ) ,
656+ ( 47 , 1 , "nonzero _pad0" ) ,
657+ ( 80 , 1 , "nonzero der_sig byte" ) ,
658+ ] ;
659+ for ( offset, value, label) in mutations {
660+ let mut buf = base;
661+ buf[ offset] = value;
662+ let err = match parse_record ( & buf, 0 ) {
663+ Err ( e) => e. to_string ( ) ,
664+ Ok ( _) => panic ! ( "{label} should have failed" ) ,
665+ } ;
666+ assert ! (
667+ err. contains( "pubkey_len 66 exceeds 65" ) ,
668+ "{label} should fail with length error, got: {err}"
669+ ) ;
670+ }
671+
672+ for offset in [ 48 , 79 , 80 , 151 , 152 , 216 , 217 , 223 ] {
673+ let mut buf = base;
674+ buf[ offset] = 1 ;
675+ let err = match parse_record ( & buf, 0 ) {
676+ Err ( e) => e. to_string ( ) ,
677+ Ok ( _) => panic ! ( "byte {offset} should have failed" ) ,
678+ } ;
679+ assert ! (
680+ err. contains( "pubkey_len 66 exceeds 65" ) ,
681+ "byte {offset} should fail with length error, got: {err}"
682+ ) ;
683+ }
684+ }
685+
686+ #[ test]
687+ fn load_records_accepts_preserved_over_capacity_reject ( ) {
688+ // Build a minimal BRSREC1 fixture with the exact over-capacity row.
689+ let path = std:: env:: temp_dir ( ) . join ( format ! (
690+ "checksig_bare_secp_preserved_row_{}.bin" ,
691+ std:: process:: id( )
692+ ) ) ;
693+ let mut file = std:: fs:: File :: create ( & path) . unwrap ( ) ;
694+ std:: io:: Write :: write_all ( & mut file, RECORD_MAGIC ) . unwrap ( ) ;
695+ std:: io:: Write :: write_all ( & mut file, & 1u64 . to_le_bytes ( ) ) . unwrap ( ) ;
696+ std:: io:: Write :: write_all ( & mut file, & preserved_over_capacity_row ( ) ) . unwrap ( ) ;
697+ drop ( file) ;
698+
699+ let records = load_records ( & path) . unwrap ( ) ;
700+ assert_eq ! ( records. len( ) , 1 ) ;
701+ assert_eq ! ( records[ 0 ] . outcome, 2 ) ;
702+ assert_eq ! ( records[ 0 ] . pubkey_len, 66 ) ;
703+
704+ std:: fs:: remove_file ( & path) . ok ( ) ;
705+ }
706+
707+ #[ test]
708+ fn record_to_bare_input_skips_over_capacity_outcome_two ( ) {
709+ // The exact over-capacity reason-1 record is outcome 2, so the
710+ // production helper must return None before any capacity-indexed
711+ // public-key slice. Removing the outcome-2 guard would make this
712+ // test panic because pubkey_len (66) exceeds the fixed 65-byte array.
713+ let rec = parse_record ( & preserved_over_capacity_row ( ) , 0 ) . unwrap ( ) ;
714+ assert_eq ! ( rec. outcome, 2 ) ;
715+ assert ! ( rec. pubkey_len > 65 ) ;
716+ let input = record_to_bare_input ( & rec) . unwrap ( ) ;
717+ assert ! (
718+ input. is_none( ) ,
719+ "outcome-2 record must be skipped before copy"
720+ ) ;
721+ }
722+
723+ #[ test]
724+ fn record_to_bare_input_accepts_ordinary_record ( ) {
725+ // Ordinary outcome-1 records are converted to a real FFI input.
726+ let buf = valid_buf ( ) ;
727+ let rec = parse_record ( & buf, 0 ) . unwrap ( ) ;
728+ assert_eq ! ( rec. outcome, 1 ) ;
729+ let input = record_to_bare_input ( & rec) . unwrap ( ) . unwrap ( ) ;
730+ assert_eq ! ( input. expected, 1 ) ;
731+ assert_eq ! ( input. der_len, 72 ) ;
732+ assert_eq ! ( input. pubkey_len, 65 ) ;
733+ }
551734}
0 commit comments