From e6b3ec338dcd53b139af9750903594bd110a2ac9 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Fri, 26 Jun 2026 01:34:26 -0400 Subject: [PATCH] fix(kona): graceful OOG on precompile gas overspend in fpvm provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fpvm precompile provider's hand-rolled `run` panicked via `assert!(underflow, "Gas underflow is not possible")` if a precompile reported `gas_used > gas_limit`. revm v40's `EthPrecompiles::run` (which op-revm inherits by delegation) instead gracefully out-of-gases this case (`spend_all` + `PrecompileOOG`). The branch is unreachable for the registered precompile set — each checks cost <= gas_limit before returning `Ok` — so there is no consensus impact. But matching upstream keeps the FPVM and op-reth aligned by construction rather than by audit, and removes a foot-gun where a future precompile that omits the check would halt the fault-proof program instead of deterministically OOG-ing. --- .../src/fpvm_evm/precompiles/provider.rs | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/rust/kona/bin/client/src/fpvm_evm/precompiles/provider.rs b/rust/kona/bin/client/src/fpvm_evm/precompiles/provider.rs index cdc13fd6e01..bf652d810fe 100644 --- a/rust/kona/bin/client/src/fpvm_evm/precompiles/provider.rs +++ b/rust/kona/bin/client/src/fpvm_evm/precompiles/provider.rs @@ -143,11 +143,17 @@ where } else { InstructionResult::PrecompileError }; - } else { - let underflow = result.gas.record_regular_cost(output.gas_used); - assert!(underflow, "Gas underflow is not possible"); + } else if result.gas.record_regular_cost(output.gas_used) { result.result = InstructionResult::Return; result.output = output.bytes; + } else { + // Mirror revm's `EthPrecompiles::run`: a precompile reporting more gas than the + // call limit gracefully out-of-gases instead of panicking. Unreachable for the + // registered precompile set (each checks cost <= gas_limit before returning `Ok`), + // but matching upstream keeps the FPVM and op-reth aligned by construction and + // avoids halting the fault-proof program on a future precompile that omits the check. + result.gas.spend_all(); + result.result = InstructionResult::PrecompileOOG; } Ok(Some(result)) @@ -396,6 +402,51 @@ mod test { assert_eq!(interpreter_result.output.as_ref(), b"mock"); } + /// A mock accelerated precompile that reports spending more gas than the call limit. + fn overspending_accelerated_precompile( + _input: &[u8], + gas_limit: u64, + _hint_writer: &H, + _oracle_reader: &O, + ) -> EthPrecompileResult + where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, + { + Ok(revm::precompile::EthPrecompileOutput::new( + gas_limit.saturating_add(1), + Bytes::from_static(b"overspend"), + )) + } + + #[test] + fn test_run_overspending_precompile_oogs_without_panicking() { + let (hint_chan, preimage_chan) = ( + kona_preimage::BidirectionalChannel::new().unwrap(), + kona_preimage::BidirectionalChannel::new().unwrap(), + ); + let hint_writer = kona_preimage::HintWriter::new(hint_chan.client); + let oracle_reader = kona_preimage::OracleReader::new(preimage_chan.client); + + let mut ctx = create_test_context(); + + let mut precompiles = + OpFpvmPrecompiles::new_with_spec(OpSpecId::BEDROCK, hint_writer, oracle_reader); + + // A precompile that reports gas_used > gas_limit must gracefully OOG, matching + // revm's `EthPrecompiles::run`, rather than panicking the fault-proof program. + precompiles + .accelerated_precompiles + .insert(ECRECOVER_ADDR, overspending_accelerated_precompile); + + let call_inputs = create_call_inputs(ECRECOVER_ADDR, Bytes::from_static(b"test"), 1000); + + let result = precompiles.run(&mut ctx, &call_inputs).unwrap().unwrap(); + assert_eq!(result.result, InstructionResult::PrecompileOOG); + assert!(result.output.is_empty()); + assert_eq!(result.gas.remaining(), 0); + } + #[test] fn test_run_default_precompile_sha256() { let (hint_chan, preimage_chan) = (