77//! the two backends cannot share a binary).
88//!
99//! Two idempotent phases:
10- //! 1. **Extract** (skipped when the corpus file already exists): stream
11- //! mainnet blocks `0..=150_000` from a local `bitcoind -rest` endpoint,
12- //! maintain an in-memory outpoint -> prevout map, and keep the top N
13- //! (default 300) blocks by non-coinbase input count, together with every
14- //! spent prevout (script bytes + amount). Coinbase inputs have no prevout
15- //! and are excluded from sampling and measurement.
10+ //! 1. **Extract** (skipped when the corpus file already exists): replay
11+ //! mainnet blocks `0..=--stop-height` from a local `bitcoind -rest`
12+ //! endpoint to seed an in-memory outpoint map, then keep the top
13+ //! `--sample-count` blocks in `--start-height..=--stop-height` by
14+ //! non-coinbase input count, together with every spent prevout.
15+ //! Extraction requires `--stop-height`, `--sample-count`, and
16+ //! `--min-inputs`. Coinbase inputs have no prevout and are excluded from
17+ //! sampling and measurement.
1618//! 2. **Measure**: verify every sampled non-coinbase transaction's scripts
17- //! through `KernelContext::verify_tx` at rayon widths 1/8/32 (a dedicated
18- //! `ThreadPoolBuilder` pool per width), with per-height flags derived
19- //! exactly as production's `compute_verify_flags` derives them . Every
20- //! pristine input must verify OK; any rejection aborts loudly with
19+ //! through `KernelContext::verify_tx` at rayon widths 1/8/32, with one
20+ //! dedicated `ThreadPoolBuilder` pool per width and mainnet
21+ //! activation-height flags matching the active chain . Every pristine input
22+ //! must verify OK; any rejection aborts loudly with
2123//! height/txid/input index. The timed window includes the per-tx
2224//! serialization + `bitcoinkernel::Transaction` parse inside `verify_tx`
2325//! (production pays both per tx) but excludes corpus load and work-item
2426//! construction (production amortizes block decode and prevout lookup in
2527//! its own pipeline stages).
2628//!
29+ //! CLI:
30+ //! --rest-url <host:port> [127.0.0.1:8332]
31+ //! --corpus PATH (required)
32+ //! --output PATH (required)
33+ //! --start-height <u32> [0]
34+ //! --stop-height <u32> (required when extracting)
35+ //! --sample-count <usize> (required when extracting)
36+ //! --min-inputs <usize> (required when extracting)
37+ //!
2738//! Corpus format (single file, all integers little-endian):
2839//! ```text
2940//! magic b"KSPIKE1\0" (8 bytes)
@@ -49,30 +60,29 @@ use std::time::{Duration, Instant};
4960
5061use anyhow:: { Context as _, Result , bail} ;
5162use bitcoin:: consensus:: Decodable as _;
63+ use bitcoin:: hashes:: Hash as _;
5264use bitcoin:: { Amount , OutPoint , ScriptBuf , TxOut } ;
5365use bitcoin_rs_consensus:: UtxoView ;
5466use bitcoin_rs_consensus:: kernel:: KernelContext ;
55- use bitcoin_rs_primitives:: { Network , Tx } ;
67+ use bitcoin_rs_primitives:: { Hash256 , Network , Tx } ;
5668use bitcoin_rs_script:: VerifyFlags ;
5769use rayon:: prelude:: * ;
5870use serde_json:: json;
5971
6072const CORPUS_MAGIC : & [ u8 ; 8 ] = b"KSPIKE1\0 " ;
61- const STOP_HEIGHT : u32 = 150_000 ;
62- const SAMPLE_BLOCKS : usize = 300 ;
63- const MIN_CORPUS_INPUTS : usize = 20_000 ;
6473const THREAD_WIDTHS : [ usize ; 3 ] = [ 1 , 8 , 32 ] ;
6574
6675fn main ( ) -> Result < ( ) > {
6776 let args = Args :: parse ( std:: env:: args_os ( ) . skip ( 1 ) ) ?;
6877
69- if args. corpus . exists ( ) {
78+ let needs_extract = !args. corpus . exists ( ) ;
79+ if needs_extract {
80+ extract_corpus ( & args) ?;
81+ } else {
7082 eprintln ! (
7183 "corpus {} exists; skipping extraction" ,
7284 args. corpus. display( )
7385 ) ;
74- } else {
75- extract_corpus ( & args) ?;
7686 }
7787
7888 let corpus = load_corpus ( & args. corpus ) ?;
@@ -81,9 +91,7 @@ fn main() -> Result<()> {
8191
8292 let rendered = serde_json:: to_string_pretty ( & report) . context ( "render report JSON" ) ?;
8393 println ! ( "{rendered}" ) ;
84- if let Some ( parent) = args. output . parent ( ) {
85- std:: fs:: create_dir_all ( parent) . with_context ( || format ! ( "create {}" , parent. display( ) ) ) ?;
86- }
94+ ensure_parent ( & args. output ) ?;
8795 std:: fs:: write ( & args. output , rendered + "\n " )
8896 . with_context ( || format ! ( "write {}" , args. output. display( ) ) ) ?;
8997 Ok ( ( ) )
@@ -94,31 +102,68 @@ struct Args {
94102 rest_url : String ,
95103 corpus : PathBuf ,
96104 output : PathBuf ,
105+ start_height : u32 ,
106+ stop_height : Option < u32 > ,
107+ sample_count : Option < usize > ,
108+ min_inputs : Option < usize > ,
97109}
98110
99111impl Args {
100112 fn parse ( args : impl IntoIterator < Item = OsString > ) -> Result < Self > {
101- let mut parsed = Self {
102- rest_url : "127.0.0.1:8332" . to_owned ( ) ,
103- corpus : PathBuf :: from ( "/home/alpha/bench-g14/results/u0-spike-corpus/corpus.bin" ) ,
104- output : PathBuf :: from ( "/home/alpha/bench-g14/results/u0-kernel-spike.json" ) ,
105- } ;
113+ let mut rest_url = "127.0.0.1:8332" . to_owned ( ) ;
114+ let mut corpus: Option < PathBuf > = None ;
115+ let mut output: Option < PathBuf > = None ;
116+ let mut start_height: Option < u32 > = None ;
117+ let mut stop_height: Option < u32 > = None ;
118+ let mut sample_count: Option < usize > = None ;
119+ let mut min_inputs: Option < usize > = None ;
106120 let mut args = args. into_iter ( ) ;
107121 while let Some ( arg) = args. next ( ) {
108122 let arg = arg
109123 . into_string ( )
110124 . map_err ( |value| anyhow:: anyhow!( "argument is not UTF-8: {}" , value. display( ) ) ) ?;
111125 match arg. as_str ( ) {
112- "--rest-url" => parsed. rest_url = next_arg ( & mut args, "--rest-url" ) ?,
113- "--corpus" => parsed. corpus = PathBuf :: from ( next_arg ( & mut args, "--corpus" ) ?) ,
114- "--output" => parsed. output = PathBuf :: from ( next_arg ( & mut args, "--output" ) ?) ,
126+ "--rest-url" => rest_url = next_arg ( & mut args, "--rest-url" ) ?,
127+ "--corpus" => corpus = Some ( PathBuf :: from ( next_arg ( & mut args, "--corpus" ) ?) ) ,
128+ "--output" => output = Some ( PathBuf :: from ( next_arg ( & mut args, "--output" ) ?) ) ,
129+ "--start-height" => {
130+ start_height = Some ( parse_u32 (
131+ & next_arg ( & mut args, "--start-height" ) ?,
132+ "--start-height" ,
133+ ) ?) ;
134+ }
135+ "--stop-height" => {
136+ stop_height = Some ( parse_u32 (
137+ & next_arg ( & mut args, "--stop-height" ) ?,
138+ "--stop-height" ,
139+ ) ?) ;
140+ }
141+ "--sample-count" => {
142+ sample_count = Some ( parse_usize (
143+ & next_arg ( & mut args, "--sample-count" ) ?,
144+ "--sample-count" ,
145+ ) ?) ;
146+ }
147+ "--min-inputs" => {
148+ min_inputs = Some ( parse_usize (
149+ & next_arg ( & mut args, "--min-inputs" ) ?,
150+ "--min-inputs" ,
151+ ) ?) ;
152+ }
115153 other => bail ! (
116- "unknown argument: {other}\n usage: kernel_verify_spike \
117- [--rest-url <host:port>] [--corpus <path>] [--output <path>]"
154+ "unknown argument: {other}\n usage: kernel_verify_spike --corpus <path> --output <path> [--rest-url <host:port>] [--start-height <u32>] [--stop-height <u32>] [--sample-count <usize>] [--min-inputs <usize>]"
118155 ) ,
119156 }
120157 }
121- Ok ( parsed)
158+ Ok ( Self {
159+ rest_url,
160+ corpus : corpus. context ( "--corpus is required" ) ?,
161+ output : output. context ( "--output is required" ) ?,
162+ start_height : start_height. unwrap_or ( 0 ) ,
163+ stop_height,
164+ sample_count,
165+ min_inputs,
166+ } )
122167 }
123168}
124169
@@ -129,14 +174,32 @@ fn next_arg(args: &mut impl Iterator<Item = OsString>, name: &str) -> Result<Str
129174 . map_err ( |value| anyhow:: anyhow!( "{name} value is not UTF-8: {}" , value. display( ) ) )
130175}
131176
132- /// Mirrors `compute_verify_flags` in `crates/node/src/apply.rs`: P2SH is
133- /// always-on for supported validation paths; DERSIG/CLTV/CSV/WITNESS/TAPROOT
134- /// gate on activation height. Production resolves CSV/segwit through BIP9
135- /// contextual state with these same height predicates as fallback; at the
136- /// corpus heights (<= 150k, far below every activation) the two derivations
137- /// are identical and every flag except P2SH is off.
138- fn production_verify_flags ( network : Network , height : u32 ) -> VerifyFlags {
139- let mut flags = VerifyFlags :: P2SH ;
177+ fn parse_u32 ( value : & str , name : & str ) -> Result < u32 > {
178+ value
179+ . parse :: < u32 > ( )
180+ . with_context ( || format ! ( "{name} must be a non-negative 32-bit integer, got {value}" ) )
181+ }
182+
183+ fn parse_usize ( value : & str , name : & str ) -> Result < usize > {
184+ value
185+ . parse :: < usize > ( )
186+ . with_context ( || format ! ( "{name} must be a non-negative integer, got {value}" ) )
187+ }
188+
189+ fn ensure_parent ( path : & Path ) -> Result < ( ) > {
190+ if let Some ( parent) = path. parent ( ) {
191+ std:: fs:: create_dir_all ( parent) . with_context ( || format ! ( "create {}" , parent. display( ) ) ) ?;
192+ }
193+ Ok ( ( ) )
194+ }
195+
196+ /// Uses the mainnet activation-height fallback, including Core's hash-pinned
197+ /// BIP16 exception. Production derives BIP9 state from the active header chain.
198+ fn production_verify_flags ( network : Network , height : u32 , block_hash : Hash256 ) -> VerifyFlags {
199+ let mut flags = VerifyFlags :: NONE ;
200+ if !network. is_bip16_p2sh_exception ( block_hash) {
201+ flags = flags. union ( VerifyFlags :: P2SH ) ;
202+ }
140203 if network. is_bip66_active ( height) {
141204 flags = flags. union ( VerifyFlags :: DERSIG ) ;
142205 }
@@ -173,9 +236,32 @@ struct SampleBlock {
173236}
174237
175238fn extract_corpus ( args : & Args ) -> Result < ( ) > {
239+ let stop_height = args
240+ . stop_height
241+ . context ( "--stop-height is required to extract a new corpus" ) ?;
242+ let sample_count = args
243+ . sample_count
244+ . context ( "--sample-count is required to extract a new corpus" ) ?;
245+ let min_inputs = args
246+ . min_inputs
247+ . context ( "--min-inputs is required to extract a new corpus" ) ?;
248+
249+ if stop_height < args. start_height {
250+ bail ! (
251+ "inconsistent bounds: stop-height {stop_height} < start-height {}" ,
252+ args. start_height
253+ ) ;
254+ }
255+ if sample_count == 0 {
256+ bail ! ( "--sample-count must be greater than zero" ) ;
257+ }
258+ if min_inputs == 0 {
259+ bail ! ( "--min-inputs must be greater than zero" ) ;
260+ }
261+
176262 eprintln ! (
177- "extracting corpus: streaming blocks 0..={STOP_HEIGHT } from {}" ,
178- args. rest_url
263+ "extracting corpus: replaying blocks 0..={stop_height}, sampling {}..={stop_height } from {}" ,
264+ args. start_height , args . rest_url
179265 ) ;
180266 let started = Instant :: now ( ) ;
181267 let mut client = RestClient :: connect ( & args. rest_url ) ?;
@@ -184,7 +270,7 @@ fn extract_corpus(args: &Args) -> Result<()> {
184270 let mut keys: BinaryHeap < Reverse < ( usize , u32 ) > > = BinaryHeap :: new ( ) ;
185271 let mut candidates: hashbrown:: HashMap < u32 , SampleBlock > = hashbrown:: HashMap :: new ( ) ;
186272
187- for height in 0 ..=STOP_HEIGHT {
273+ for height in 0 ..=stop_height {
188274 let raw = fetch_block ( & mut client, height) ?;
189275 let block = bitcoin:: Block :: consensus_decode ( & mut std:: io:: Cursor :: new ( raw. as_slice ( ) ) )
190276 . with_context ( || format ! ( "decode block at height {height}" ) ) ?;
@@ -217,13 +303,13 @@ fn extract_corpus(args: &Args) -> Result<()> {
217303 }
218304
219305 let input_count = prevouts. len ( ) ;
220- if input_count > 0 {
306+ if height >= args . start_height && input_count > 0 {
221307 let candidate = SampleBlock {
222308 height,
223309 raw,
224310 prevouts,
225311 } ;
226- if keys. len ( ) < SAMPLE_BLOCKS {
312+ if keys. len ( ) < sample_count {
227313 keys. push ( Reverse ( ( input_count, height) ) ) ;
228314 candidates. insert ( height, candidate) ;
229315 } else if let Some ( & Reverse ( ( min_count, min_height) ) ) = keys. peek ( )
@@ -254,10 +340,8 @@ fn extract_corpus(args: &Args) -> Result<()> {
254340 samples. len( ) ,
255341 started. elapsed( )
256342 ) ;
257- if total_inputs < MIN_CORPUS_INPUTS {
258- bail ! (
259- "corpus too small: {total_inputs} non-coinbase inputs < required {MIN_CORPUS_INPUTS}"
260- ) ;
343+ if total_inputs < min_inputs {
344+ bail ! ( "corpus too small: {total_inputs} non-coinbase inputs < required {min_inputs}" ) ;
261345 }
262346 write_corpus ( & args. corpus , & samples)
263347}
@@ -276,9 +360,7 @@ fn fetch_block(client: &mut RestClient, height: u32) -> Result<Vec<u8>> {
276360}
277361
278362fn write_corpus ( path : & Path , samples : & [ SampleBlock ] ) -> Result < ( ) > {
279- if let Some ( parent) = path. parent ( ) {
280- std:: fs:: create_dir_all ( parent) . with_context ( || format ! ( "create {}" , parent. display( ) ) ) ?;
281- }
363+ ensure_parent ( path) ?;
282364 let file = std:: fs:: File :: create ( path) . with_context ( || format ! ( "create {}" , path. display( ) ) ) ?;
283365 let mut writer = BufWriter :: new ( file) ;
284366 writer. write_all ( CORPUS_MAGIC ) ?;
@@ -379,7 +461,8 @@ fn build_work_items(corpus: &[SampleBlock]) -> Result<Vec<WorkItem>> {
379461 let block =
380462 bitcoin:: Block :: consensus_decode ( & mut std:: io:: Cursor :: new ( sample. raw . as_slice ( ) ) )
381463 . with_context ( || format ! ( "decode corpus block at height {}" , sample. height) ) ?;
382- let flags = production_verify_flags ( Network :: Mainnet , sample. height ) ;
464+ let block_hash = Hash256 :: from_le_bytes ( block. block_hash ( ) . as_byte_array ( ) ) ;
465+ let flags = production_verify_flags ( Network :: Mainnet , sample. height , block_hash) ;
383466 let mut prevouts = sample. prevouts . iter ( ) ;
384467 for tx in & block. txdata {
385468 if tx. is_coinbase ( ) {
@@ -522,10 +605,14 @@ fn measure(corpus: &[SampleBlock], items: &[WorkItem], args: &Args) -> Result<se
522605 }
523606
524607 Ok ( json ! ( {
525- "schema" : "u0-kernel-verify-spike-v1 " ,
608+ "schema" : "u0-kernel-verify-spike-v2 " ,
526609 "measurement_target" : "bitcoinkernel per-input script verify" ,
527610 "git_head" : git_head( ) . ok( ) ,
528611 "comparator" : "recorded ~65 us/input effective-serial, bitcoinconsensus backend, same machine" ,
612+ "start_height" : args. start_height,
613+ "stop_height" : args. stop_height,
614+ "sample_count" : args. sample_count,
615+ "min_inputs" : args. min_inputs,
529616 "corpus" : {
530617 "path" : args. corpus. display( ) . to_string( ) ,
531618 "block_count" : corpus. len( ) ,
@@ -538,13 +625,15 @@ fn measure(corpus: &[SampleBlock], items: &[WorkItem], args: &Args) -> Result<se
538625 } ) )
539626}
540627
628+ // Benchmark ratios intentionally trade integer precision for a fractional result.
629+ #[ allow( clippy:: as_conversions, clippy:: cast_precision_loss) ]
541630fn duration_us_per_input ( elapsed : Duration , total_inputs : usize ) -> Result < f64 > {
542- let micros = u32 :: try_from ( elapsed. as_micros ( ) ) . context ( "elapsed micros exceed u32 " ) ?;
543- let inputs = u32 :: try_from ( total_inputs) . context ( "input count exceeds u32 " ) ?;
631+ let micros = u64 :: try_from ( elapsed. as_micros ( ) ) . context ( "elapsed micros exceed u64 " ) ?;
632+ let inputs = u64 :: try_from ( total_inputs) . context ( "input count exceeds u64 " ) ?;
544633 if inputs == 0 {
545634 bail ! ( "corpus contains zero inputs" ) ;
546635 }
547- Ok ( f64 :: from ( micros) / f64 :: from ( inputs) )
636+ Ok ( ( micros as f64 ) / ( inputs as f64 ) )
548637}
549638
550639fn git_head ( ) -> Result < String > {
0 commit comments