Skip to content

Commit 52944b7

Browse files
committed
Make import faster
1 parent 808251d commit 52944b7

7 files changed

Lines changed: 441 additions & 95 deletions

File tree

app/services/eth_block_importer.rb

Lines changed: 83 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -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

app/services/geth_driver.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ def propose_block(
2222
finalized_block:
2323
)
2424
# Create filler blocks if necessary and update head_block
25+
ImportProfiler.start("create_filler_blocks")
2526
filler_blocks = create_filler_blocks(
2627
head_block: head_block,
2728
new_ethscriptions_block: new_ethscriptions_block,
2829
safe_block: safe_block,
2930
finalized_block: finalized_block
3031
)
32+
ImportProfiler.stop("create_filler_blocks")
3133

3234
head_block = filler_blocks.last || head_block
3335

@@ -74,7 +76,10 @@ def propose_block(
7476
payload_attributes = ByteString.deep_hexify(payload_attributes)
7577
fork_choice_state = ByteString.deep_hexify(fork_choice_state)
7678

79+
ImportProfiler.start("engine_forkchoiceUpdated_1")
7780
fork_choice_response = client.call("engine_forkchoiceUpdatedV#{version}", [fork_choice_state, payload_attributes])
81+
ImportProfiler.stop("engine_forkchoiceUpdated_1")
82+
7883
if fork_choice_response['error']
7984
raise "Fork choice update failed: #{fork_choice_response['error']}"
8085
end
@@ -84,7 +89,10 @@ def propose_block(
8489
raise "Fork choice update did not return a payload ID"
8590
end
8691

92+
ImportProfiler.start("engine_getPayload")
8793
get_payload_response = client.call("engine_getPayloadV#{version}", [payload_id])
94+
ImportProfiler.stop("engine_getPayload")
95+
8896
if get_payload_response['error']
8997
raise "Get payload failed: #{get_payload_response['error']}"
9098
end
@@ -104,8 +112,10 @@ def propose_block(
104112

105113
new_payload_request = ByteString.deep_hexify(new_payload_request)
106114

115+
ImportProfiler.start("engine_newPayload")
107116
new_payload_response = client.call("engine_newPayloadV#{version}", new_payload_request)
108-
117+
ImportProfiler.stop("engine_newPayload")
118+
109119
status = new_payload_response['status']
110120
unless status == 'VALID'
111121
raise "New payload was not valid: #{status}"
@@ -126,7 +136,9 @@ def propose_block(
126136

127137
fork_choice_state = ByteString.deep_hexify(fork_choice_state)
128138

139+
ImportProfiler.start("engine_forkchoiceUpdated_2")
129140
fork_choice_response = client.call("engine_forkchoiceUpdatedV#{version}", [fork_choice_state, nil])
141+
ImportProfiler.stop("engine_forkchoiceUpdated_2")
130142

131143
status = fork_choice_response['payloadStatus']['status']
132144
unless status == 'VALID'

config/derive_ethscriptions_blocks.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ module Clockwork
114114
every(import_interval.seconds, 'import_ethscriptions_blocks') do
115115
importer = EthBlockImporter.new
116116

117+
# Handle Ctrl+C gracefully
118+
Signal.trap('INT') do
119+
puts "\n[#{Time.now}] Received INT signal, shutting down..."
120+
importer.shutdown
121+
exit 130
122+
end
123+
124+
Signal.trap('TERM') do
125+
puts "\n[#{Time.now}] Received TERM signal, shutting down..."
126+
importer.shutdown
127+
exit 143
128+
end
129+
117130
# Track statistics
118131
total_blocks_imported = 0
119132
total_ethscriptions = 0

0 commit comments

Comments
 (0)