@@ -7,14 +7,13 @@ class BlockNotReadyToImportError < StandardError; end
77 # Raised when a re-org is detected (parent hash mismatch)
88 class ReorgDetectedError < StandardError ; end
99
10- attr_accessor :l1_rpc_results , : ethscriptions_block_cache, :ethereum_client , :eth_block_cache , :geth_driver
11-
10+ attr_accessor :ethscriptions_block_cache , :ethereum_client , :eth_block_cache , :geth_driver
11+
1212 def initialize
13- @l1_rpc_results = { }
1413 @ethscriptions_block_cache = { }
1514 @eth_block_cache = { }
1615
17- @ethereum_client ||= EthRpcClient . new ( ENV . fetch ( 'L1_RPC_URL' ) )
16+ @ethereum_client ||= EthRpcClient . l1
1817
1918 @geth_driver = GethDriver
2019
@@ -24,7 +23,7 @@ def initialize
2423
2524 # Create a shared thread pool for validation
2625 if @validation_enabled
27- @validation_threads = ENV . fetch ( 'VALIDATION_THREADS' , '50 ' ) . to_i
26+ @validation_threads = ENV . fetch ( 'VALIDATION_THREADS' , '10 ' ) . to_i
2827 @validation_executor = Concurrent ::ThreadPoolExecutor . new (
2928 min_threads : 1 ,
3029 max_threads : @validation_threads ,
@@ -34,12 +33,29 @@ def initialize
3433 logger . info "Created validation thread pool with #{ @validation_threads } threads"
3534 end
3635
36+ # L1 prefetcher for blocks/receipts/API data
37+ @prefetcher = L1RpcPrefetcher . new (
38+ ethereum_client : @ethereum_client ,
39+ ahead : ENV . fetch ( 'L1_PREFETCH_FORWARD' , Rails . env . test? ? 5 : 20 ) . to_i ,
40+ threads : ENV . fetch ( 'L1_PREFETCH_THREADS' , Rails . env . test? ? 2 : 2 ) . to_i
41+ )
42+ @prefetched_api_data = { }
43+
3744 logger . info "EthBlockImporter initialized - Validation: #{ @validation_enabled ? 'ENABLED' : 'disabled' } "
3845
3946 MemeryExtensions . clear_all_caches!
4047
4148 set_eth_block_starting_points
4249 populate_ethscriptions_block_cache
50+
51+ unless Rails . env . test?
52+ max_block = current_max_eth_block_number
53+ if max_block && max_block > 0
54+ ImportProfiler . start ( 'prefetch_warmup' )
55+ @prefetcher . ensure_prefetched ( max_block + 1 )
56+ ImportProfiler . stop ( 'prefetch_warmup' )
57+ end
58+ end
4359 end
4460
4561 def current_max_ethscriptions_block_number
@@ -60,10 +76,14 @@ def populate_ethscriptions_block_cache
6076
6177 while epochs_found < 64 && current_block_number >= 0
6278 hex_block_number = "0x#{ current_block_number . to_s ( 16 ) } "
79+ ImportProfiler . start ( "l2_block_fetch" )
6380 block_data = geth_driver . client . call ( "eth_getBlockByNumber" , [ hex_block_number , false ] )
81+ ImportProfiler . stop ( "l2_block_fetch" )
6482 current_block = EthscriptionsBlock . from_rpc_result ( block_data )
65-
83+
84+ ImportProfiler . start ( "l1_attributes_fetch" )
6685 l1_attributes = GethDriver . client . get_l1_attributes ( current_block . number )
86+ ImportProfiler . stop ( "l1_attributes_fetch" )
6787 current_block . assign_l1_attributes ( l1_attributes )
6888
6989 ethscriptions_block_cache [ current_block . number ] = current_block
@@ -183,50 +203,15 @@ def import_blocks_until_done
183203 raise BlockNotReadyToImportError . new ( "Block not ready" )
184204 end
185205
186- populate_l1_rpc_results ( block_numbers )
187-
188206 import_blocks ( block_numbers )
189207 rescue BlockNotReadyToImportError => e
190208 puts "#{ e . message } . Stopping import."
209+ ImportProfiler . report if ImportProfiler . enabled?
191210 break
192211 end
193212 end
194213 end
195214
196- def populate_l1_rpc_results ( block_numbers )
197- next_start_block = block_numbers . last + 1
198- next_block_numbers = ( next_start_block ...( next_start_block + import_batch_size ) ) . to_a
199-
200- blocks_to_import = block_numbers
201-
202- if blocks_behind > 1
203- blocks_to_import += next_block_numbers . select do |num |
204- num <= current_block_number
205- end
206- end
207-
208- blocks_to_import -= l1_rpc_results . keys
209-
210- l1_rpc_results . reverse_merge! ( get_blocks_promises ( blocks_to_import ) )
211- end
212-
213- def get_blocks_promises ( block_numbers )
214- block_numbers . map do |block_number |
215- block_promise = Concurrent ::Promise . execute do
216- ethereum_client . get_block ( block_number , true )
217- end
218-
219- receipt_promise = Concurrent ::Promise . execute do
220- ethereum_client . get_transaction_receipts ( block_number )
221- end
222-
223- [ block_number , {
224- block : block_promise ,
225- receipts : receipt_promise
226- } . with_indifferent_access ]
227- end . to_h
228- end
229-
230215 def fetch_block_from_cache ( block_number )
231216 block_number = [ block_number , 0 ] . max
232217
@@ -246,6 +231,12 @@ def prune_caches
246231 ethscriptions_block_cache . delete_if do |_ , ethscriptions_block |
247232 ethscriptions_block . eth_block_number < oldest_eth_block_to_keep
248233 end
234+
235+ # Clean up prefetcher and API data caches
236+ if oldest_eth_block_to_keep
237+ @prefetcher . clear_older_than ( oldest_eth_block_to_keep )
238+ @prefetched_api_data . delete_if { |block_num , _ | block_num < oldest_eth_block_to_keep }
239+ end
249240 end
250241
251242 def current_ethscriptions_block ( type )
@@ -289,48 +280,50 @@ def current_facet_finalized_block
289280 end
290281
291282 def import_blocks ( block_numbers )
283+ ImportProfiler . start ( "import_blocks_total" )
284+
285+ # Initialize seen creates for this batch (needed by prefetcher threads)
286+ EthscriptionTransaction . reset_seen_creates!
287+
292288 logger . info "Block Importer: importing blocks #{ block_numbers . join ( ', ' ) } "
293289 start = Time . current
294-
295- block_responses = l1_rpc_results . select do |block_number , _ |
296- block_numbers . include? ( block_number )
297- end . to_h . transform_values do |hsh |
298- hsh . transform_values ( &:value! )
299- end
300-
301- l1_rpc_results . reject! { |block_number , _ | block_responses . key? ( block_number ) }
302-
290+
291+ ImportProfiler . start ( "prefetch_ensure" )
292+ @prefetcher . ensure_prefetched ( block_numbers . first )
293+ ImportProfiler . stop ( "prefetch_ensure" )
294+
303295 eth_blocks = [ ]
304296 ethscriptions_blocks = [ ]
305297 res = [ ]
306298
307- block_numbers . each . with_index do |block_number , index |
308- block_response = block_responses [ block_number ]
309-
310- block_result = block_response [ 'block' ]
311- receipt_result = block_response [ 'receipts' ]
312-
299+ block_numbers . each do |block_number |
300+ ImportProfiler . start ( "prefetch_fetch" )
301+ response = @prefetcher . fetch ( block_number )
302+ ImportProfiler . stop ( "prefetch_fetch" )
303+
304+ eth_block = response [ :eth_block ]
305+ ethscriptions_block = response [ :ethscriptions_block ]
306+ ethscription_txs = response [ :ethscription_txs ]
307+
308+ ethscription_txs . each { |tx | tx . ethscriptions_block = ethscriptions_block }
309+
310+ if @validation_enabled && response [ :api_data ]
311+ @prefetched_api_data [ block_number ] = response [ :api_data ]
312+ end
313+
313314 parent_eth_block = eth_block_cache [ block_number - 1 ]
314-
315- if parent_eth_block && parent_eth_block . block_hash != Hash32 . from_hex ( block_result [ 'parentHash' ] )
315+
316+ if parent_eth_block && parent_eth_block . block_hash != eth_block . parent_hash
316317 logger . info "Reorg detected at block #{ block_number } "
317318 raise ReorgDetectedError
318319 end
319-
320- eth_block = EthBlock . from_rpc_result ( block_result )
321320
322- ethscriptions_block = EthscriptionsBlock . from_eth_block ( eth_block )
323-
324- ethscription_txs = EthTransaction . ethscription_txs_from_rpc_results ( block_result , receipt_result , ethscriptions_block )
325-
326- ethscription_txs . each do |ethscription_tx |
327- ethscription_tx . ethscriptions_block = ethscriptions_block
328- end
329-
321+ ImportProfiler . start ( "propose_ethscriptions_block" )
330322 imported_ethscriptions_blocks = propose_ethscriptions_block (
331323 ethscriptions_block : ethscriptions_block ,
332324 ethscription_txs : ethscription_txs
333325 )
326+ ImportProfiler . stop ( "propose_ethscriptions_block" )
334327
335328 logger . debug "Block #{ block_number } : Found #{ ethscription_txs . length } ethscription txs, created #{ imported_ethscriptions_blocks . length } L2 blocks"
336329
@@ -368,13 +361,22 @@ def import_blocks(block_numbers)
368361 logger . info "Total gas used: #{ total_gas_millions } million (avg: #{ average_gas_per_block_millions } million / block)"
369362 logger . info "Gas per second: #{ gas_per_second_millions } million / s"
370363
364+ if block_numbers . length >= 10
365+ stats = @prefetcher . stats
366+ logger . info "Prefetcher stats: promises=#{ stats [ :promises_total ] } " \
367+ "(fulfilled=#{ stats [ :promises_fulfilled ] } , pending=#{ stats [ :promises_pending ] } , " \
368+ "threads_active=#{ stats [ :threads_active ] } , queued=#{ stats [ :threads_queued ] } )"
369+ end
370+
371371 # Validate imported blocks if enabled
372372 if @validation_enabled
373373 if ethscriptions_blocks . any?
374+ ImportProfiler . start ( "validation_wall_clock" )
374375 start_validation = Time . current
375376 logger . info "Starting parallel validation for #{ block_numbers . length } blocks with #{ ethscriptions_blocks . length } L2 blocks"
376377 validate_imported_blocks ( block_numbers , ethscriptions_blocks )
377378 validation_time = Time . current - start_validation
379+ ImportProfiler . stop ( "validation_wall_clock" )
378380 logger . info "Validation completed in #{ validation_time . round ( 2 ) } s (#{ ( block_numbers . length / validation_time ) . round ( 2 ) } blocks/s)"
379381 else
380382 logger . info "Validation enabled but no ethscriptions blocks to validate"
@@ -383,14 +385,19 @@ def import_blocks(block_numbers)
383385 logger . warn "Validation requested but not enabled in importer initialization"
384386 end
385387
388+ ImportProfiler . stop ( "import_blocks_total" )
389+
390+ # Generate profiling report after each batch
391+ if ImportProfiler . enabled?
392+ ImportProfiler . report
393+ ImportProfiler . reset
394+ end
395+
386396 [ ethscriptions_blocks , eth_blocks ]
387397 end
388398
389399 def import_next_block
390400 block_number = next_block_to_import
391-
392- populate_l1_rpc_results ( [ block_number ] )
393-
394401 import_blocks ( [ block_number ] )
395402 end
396403
@@ -450,7 +457,8 @@ def validate_imported_blocks(l1_block_numbers, l2_blocks)
450457 else
451458 begin
452459 validator = BlockValidator . new
453- result = validator . validate_l1_block ( l1_block_num , l2_hashes )
460+ api_data = @prefetched_api_data . delete ( l1_block_num )
461+ result = validator . validate_l1_block ( l1_block_num , l2_hashes , expected_override : api_data )
454462
455463 if result . stats [ :api_unavailable ]
456464 {
@@ -544,6 +552,8 @@ def validation_summary
544552 end
545553
546554 def shutdown
555+ @prefetcher &.shutdown
556+
547557 if @validation_executor
548558 logger . info "Shutting down validation thread pool..."
549559 @validation_executor . shutdown
0 commit comments