11class BlockValidator
22 attr_reader :errors , :stats
33
4+ # Exception for transient errors that should trigger retries
5+ class TransientValidationError < StandardError ; end
6+
47 def initialize
58 # Initialize validation state
69 reset_validation_state
@@ -29,7 +32,7 @@ def validate_l1_block(l1_block_number, l2_block_hashes)
2932 verify_storage_state ( expected , l1_block_number , historical_block_tag )
3033
3134 # Build comprehensive result with full debugging data
32- success = @errors . empty? && ! @incomplete_actual && ! expected [ :api_unavailable ]
35+ success = @errors . empty ( )
3336 validation_duration = Time . current - validation_start_time
3437
3538 # Return comprehensive debugging information
@@ -45,8 +48,6 @@ def validate_l1_block(l1_block_number, l2_block_hashes)
4548 actual_transfers : Array ( actual_events [ :transfers ] ) . size ,
4649 storage_checks : @storage_checks_performed . value ,
4750 errors_count : @errors . size ,
48- api_unavailable : expected [ :api_unavailable ] ? true : false ,
49- incomplete_actual : @incomplete_actual ,
5051
5152 # L1 to L2 block mapping
5253 l1_to_l2_mapping : {
@@ -64,8 +65,6 @@ def validate_l1_block(l1_block_number, l2_block_hashes)
6465 raw_expected_data : {
6566 creations : sanitize_for_json ( expected [ :creations ] || [ ] ) ,
6667 transfers : sanitize_for_json ( expected [ :transfers ] || [ ] ) ,
67- api_available : !expected [ :api_unavailable ] ,
68- api_error : expected [ :api_error ]
6968 } ,
7069
7170 raw_actual_data : {
@@ -94,7 +93,6 @@ def validate_l1_block(l1_block_number, l2_block_hashes)
9493 def reset_validation_state
9594 @errors = Concurrent ::Array . new
9695 @storage_checks_performed = Concurrent ::AtomicFixnum . new ( 0 )
97- @incomplete_actual = false
9896
9997 # Reset debugging instrumentation
10098 @debug_data = {
@@ -119,10 +117,15 @@ def load_genesis_transaction_hashes
119117
120118 def fetch_expected_data ( l1_block_number )
121119 EthscriptionsApiClient . fetch_block_data ( l1_block_number )
120+ rescue EthscriptionsApiClient ::ApiUnavailableError => e
121+ # API unavailable after exhausting all retries - this is an infrastructure issue
122+ Rails . logger . warn "API unavailable for block #{ l1_block_number } : #{ e . message } "
123+ raise TransientValidationError , e . message
122124 rescue => e
123- message = "Failed to fetch API data: #{ e . message } "
125+ # Other unexpected errors - log and continue with empty data
126+ message = "Unexpected error fetching API data: #{ e . message } "
124127 @errors << message
125- { creations : [ ] , transfers : [ ] , api_unavailable : true , api_error : message }
128+ { creations : [ ] , transfers : [ ] }
126129 end
127130
128131 def aggregate_l2_events ( block_hashes )
@@ -134,18 +137,24 @@ def aggregate_l2_events(block_hashes)
134137 begin
135138 receipts = EthRpcClient . l2 . call ( 'eth_getBlockReceipts' , [ block_hash ] )
136139 if receipts . nil?
137- @errors << "No receipts returned for L2 block #{ block_hash } "
138- @incomplete_actual = true
139- raise "No receipts returned for L2 block #{ block_hash } "
140+ error_msg = "No receipts returned for L2 block #{ block_hash } "
141+ @errors << error_msg
142+ # Treat missing receipts as potentially transient
143+ raise TransientValidationError , error_msg
140144 end
141145
142146 data = EventDecoder . decode_block_receipts ( receipts )
143147 all_creations . concat ( data [ :creations ] )
144148 all_transfers . concat ( data [ :transfers ] ) # Ethscriptions protocol transfers
145149 rescue => e
146- @errors << "Failed to get receipts for block #{ block_hash } : #{ e . message } "
147- @incomplete_actual = true
148- raise "Failed to get receipts for block #{ block_hash } : #{ e . message } "
150+ error_msg = "Failed to get receipts for block #{ block_hash } : #{ e . message } "
151+ @errors << error_msg
152+ # Classify L2 receipt fetch errors - network issues are transient
153+ if transient_error? ( e )
154+ raise TransientValidationError , error_msg
155+ else
156+ raise error_msg
157+ end
149158 end
150159 end
151160
@@ -561,6 +570,21 @@ def safe_content_preview(content, length: 50)
561570 preview + ( content . length > length ? "..." : "" )
562571 end
563572
573+ # Classify L2 RPC errors as transient (infrastructure) vs permanent (logic)
574+ def transient_error? ( error )
575+ case error
576+ # L2 RPC network errors
577+ when SocketError , Errno ::ECONNREFUSED , Errno ::ECONNRESET ,
578+ Net ::OpenTimeout , Net ::ReadTimeout , Net ::TimeoutError
579+ true
580+ # L2 RPC errors that might be transient
581+ when EthRpcClient ::HttpError , EthRpcClient ::ApiError
582+ true
583+ else
584+ false
585+ end
586+ end
587+
564588 # Sanitize data structures for JSON serialization
565589 def sanitize_for_json ( data )
566590 case data
0 commit comments