#[cfg(not(feature = "std"))]
use alloc as std;
use std::{boxed::Box, string::String, sync::Arc};
use crate::{ExternalEnvTypes, MegaContext, MegaSpecId};
use alloy_evm::{
precompiles::{DynPrecompile, PrecompilesMap},
Database,
};
use delegate::delegate;
use once_cell::race::OnceBox;
use op_revm::{OpContext, OpSpecId};
use revm::{
context::Cfg,
context_interface::ContextTr,
handler::{EthPrecompiles, PrecompileProvider},
interpreter::{Gas, InputsImpl, InterpreterResult},
precompile::Precompiles,
primitives::{Address, HashMap},
};
#[derive(Debug, Clone)]
pub struct MegaPrecompiles {
inner: EthPrecompiles,
spec: MegaSpecId,
}
impl MegaPrecompiles {
#[inline]
pub fn new_with_spec(spec: MegaSpecId) -> Self {
let inner = match spec {
MegaSpecId::EQUIVALENCE => op_revm::precompiles::isthmus(),
MegaSpecId::MINI_REX => mini_rex(),
MegaSpecId::REX |
MegaSpecId::REX1 |
MegaSpecId::REX2 |
MegaSpecId::REX3 |
MegaSpecId::REX4 |
MegaSpecId::REX5 => rex(),
};
Self { inner: EthPrecompiles { precompiles: inner, spec: spec.into_eth_spec() }, spec }
}
#[inline]
pub fn precompiles(&self) -> &'static Precompiles {
self.inner.precompiles
}
}
pub fn rex() -> &'static Precompiles {
mini_rex()
}
pub fn mini_rex() -> &'static Precompiles {
static INSTANCE: OnceBox<Precompiles> = OnceBox::new();
INSTANCE.get_or_init(|| {
let mut precompiles = op_revm::precompiles::isthmus().clone();
precompiles
.extend([revm::precompile::modexp::OSAKA, kzg_point_evaluation::KZG_POINT_EVALUATION]);
Box::new(precompiles)
})
}
pub mod kzg_point_evaluation {
use revm::{
precompile::{PrecompileError, PrecompileWithAddress},
primitives::Address,
};
pub const ADDRESS: Address = revm::precompile::kzg_point_evaluation::ADDRESS;
pub const GAS_COST: u64 = 100_000;
pub const KZG_POINT_EVALUATION: PrecompileWithAddress =
PrecompileWithAddress(ADDRESS, |input, gas_limit| {
if gas_limit < GAS_COST {
return Err(PrecompileError::OutOfGas);
}
let mut output = revm::precompile::kzg_point_evaluation::run(input, gas_limit)?;
output.gas_used = GAS_COST;
Ok(output)
});
}
impl<CTX> PrecompileProvider<CTX> for MegaPrecompiles
where
CTX: ContextTr<Cfg: Cfg<Spec = MegaSpecId>>,
{
type Output = InterpreterResult;
#[inline]
fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
if spec == self.spec {
return false;
}
*self = Self::new_with_spec(spec);
true
}
delegate! {
to self.inner {
fn run(
&mut self,
context: &mut CTX,
address: &Address,
inputs: &InputsImpl,
is_static: bool,
gas_limit: u64,
) -> Result<Option<Self::Output>, String>;
fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>>;
fn contains(&self, address: &Address) -> bool;
}
}
}
impl Default for MegaPrecompiles {
fn default() -> Self {
Self::new_with_spec(MegaSpecId::EQUIVALENCE)
}
}
impl<DB: Database, ExtEnvs: ExternalEnvTypes> PrecompileProvider<MegaContext<DB, ExtEnvs>>
for PrecompilesMap
{
type Output = InterpreterResult;
#[inline]
fn set_spec(&mut self, spec: OpSpecId) -> bool {
PrecompileProvider::<OpContext<DB>>::set_spec(self, spec)
}
#[inline]
fn run(
&mut self,
context: &mut MegaContext<DB, ExtEnvs>,
address: &Address,
inputs: &InputsImpl,
is_static: bool,
gas_limit: u64,
) -> Result<Option<Self::Output>, String> {
let is_rex5_enabled = context.spec.is_enabled(MegaSpecId::REX5);
let effective_gas_limit = if is_rex5_enabled {
let remaining = context.additional_limit.borrow().current_call_remaining_compute_gas();
gas_limit.min(remaining)
} else {
gas_limit
};
let maybe_output = PrecompileProvider::<OpContext<DB>>::run(
self,
context,
address,
inputs,
is_static,
effective_gas_limit,
)?;
Ok(maybe_output.map(|mut output| {
if is_rex5_enabled && output.result.is_ok_or_revert() {
let spent = output.gas.spent();
let refunded = output.gas.refunded();
let mut normalized = Gas::new(gas_limit);
normalized.set_spent(spent);
normalized.record_refund(refunded);
output.gas = normalized;
}
if is_rex5_enabled {
let compute_gas = if output.result.is_ok_or_revert() {
output.gas.spent()
} else if address == &kzg_point_evaluation::ADDRESS &&
output.gas.limit() >= kzg_point_evaluation::GAS_COST
{
kzg_point_evaluation::GAS_COST
} else {
output.gas.limit()
};
context.additional_limit.borrow_mut().record_compute_gas(compute_gas);
} else if context.spec.is_enabled(MegaSpecId::MINI_REX) {
context.additional_limit.borrow_mut().record_compute_gas(output.gas.spent());
}
output
}))
}
#[inline]
fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>> {
PrecompileProvider::<OpContext<DB>>::warm_addresses(self)
}
#[inline]
fn contains(&self, address: &Address) -> bool {
PrecompileProvider::<OpContext<DB>>::contains(self, address)
}
}
pub type DynPrecompilesBuilder =
Arc<dyn Fn(MegaSpecId) -> HashMap<Address, DynPrecompile> + Send + Sync>;
#[cfg(test)]
mod tests {
#[cfg(not(feature = "std"))]
use alloc as std;
use std::{rc::Rc, vec::Vec};
use super::{kzg_point_evaluation::GAS_COST, MegaPrecompiles};
use crate::{
test_utils::MemoryDatabase, AdditionalLimit, EvmTxRuntimeLimits, MegaContext, MegaSpecId,
};
use alloy_evm::precompiles::PrecompilesMap;
use alloy_primitives::Bytes;
use core::cell::RefCell;
use revm::{
handler::PrecompileProvider,
interpreter::{InputsImpl, InstructionResult},
};
use sha2::{Digest, Sha256};
fn generate_kzg_test_input() -> InputsImpl {
let commitment = hex::decode("8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7").unwrap();
let mut versioned_hash = Sha256::digest(&commitment).to_vec();
versioned_hash[0] = 0x01; let z = hex::decode("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000")
.unwrap();
let y = hex::decode("1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9")
.unwrap();
let proof = hex::decode("a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc2160744faf0070725e00b60ad9a026a15b1a8c").unwrap();
let mut input = Vec::new();
input.extend_from_slice(&versioned_hash);
input.extend_from_slice(&z);
input.extend_from_slice(&y);
input.extend_from_slice(&commitment);
input.extend_from_slice(&proof);
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
InputsImpl {
target_address: address,
bytecode_address: Some(address),
caller_address: address,
input: revm::interpreter::CallInput::Bytes(Bytes::from(input)),
call_value: Default::default(),
}
}
fn generate_invalid_proof_kzg_test_input() -> InputsImpl {
let mut inputs = generate_kzg_test_input();
let bytes = match inputs.input {
revm::interpreter::CallInput::Bytes(b) => b,
_ => panic!("expected Bytes"),
};
let mut buf = bytes.to_vec();
let last = buf.len() - 1;
buf[last] ^= 0x01;
inputs.input = revm::interpreter::CallInput::Bytes(Bytes::from(buf));
inputs
}
#[test]
fn test_kzg_precompile_sufficient_gas() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::MINI_REX);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::MINI_REX).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let result = precompiles_map.run(&mut context, &address, &inputs, true, 200_000);
assert!(result.is_ok(), "Precompile should succeed with sufficient gas");
let output = result.unwrap().unwrap();
assert!(matches!(output.result, InstructionResult::Return), "Result should be Return");
assert_eq!(output.gas.spent(), GAS_COST);
}
#[test]
fn test_kzg_precompile_exact_gas_limit() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::MINI_REX);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::MINI_REX).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let result = precompiles_map.run(&mut context, &address, &inputs, true, GAS_COST);
assert!(result.is_ok(), "Precompile should succeed with exact GAS_COST");
let output = result.unwrap().unwrap();
assert!(matches!(output.result, InstructionResult::Return), "Result should be Return");
assert_eq!(output.gas.spent(), GAS_COST);
}
#[test]
fn test_kzg_precompile_insufficient_gas() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::MINI_REX);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::MINI_REX).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let result = precompiles_map.run(&mut context, &address, &inputs, true, 50_000);
assert!(result.is_ok(), "Should not panic");
let output = result.unwrap();
assert!(output.is_some(), "Precompile should return Some(result)");
let interpreter_result = output.unwrap();
assert!(
matches!(interpreter_result.result, InstructionResult::PrecompileOOG),
"Result should be PrecompileOOG"
);
}
#[test]
fn test_kzg_precompile_one_below_gas_limit() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::MINI_REX);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::MINI_REX).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let result = precompiles_map.run(&mut context, &address, &inputs, true, GAS_COST - 1);
assert!(result.is_ok(), "Should not panic");
let output = result.unwrap();
assert!(output.is_some(), "Precompile should return Some(result)");
let interpreter_result = output.unwrap();
assert!(
matches!(interpreter_result.result, InstructionResult::PrecompileOOG),
"Result should be PrecompileOOG"
);
}
#[test]
fn test_kzg_precompile_zero_gas() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::MINI_REX);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::MINI_REX).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let result = precompiles_map.run(&mut context, &address, &inputs, true, 0);
assert!(result.is_ok(), "Should not panic");
let output = result.unwrap();
assert!(output.is_some(), "Precompile should return Some(result)");
let interpreter_result = output.unwrap();
assert!(
matches!(interpreter_result.result, InstructionResult::PrecompileOOG),
"Result should be PrecompileOOG"
);
}
fn set_tx_compute_gas_limit<DB: alloy_evm::Database, ExtEnvs: crate::ExternalEnvTypes>(
context: &mut MegaContext<DB, ExtEnvs>,
spec: MegaSpecId,
limit: u64,
) {
let tx_limits = EvmTxRuntimeLimits {
tx_compute_gas_limit: limit,
..EvmTxRuntimeLimits::from_spec(spec)
};
context.additional_limit = Rc::new(RefCell::new(AdditionalLimit::new(spec, tx_limits)));
}
#[test]
fn test_kzg_precompile_rex5_compute_gas_cap_succeeds() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::REX5);
set_tx_compute_gas_limit(&mut context, MegaSpecId::REX5, GAS_COST + 1_000);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::REX5).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let forwarded_gas = 500_000u64;
let result = precompiles_map.run(&mut context, &address, &inputs, true, forwarded_gas);
let output = result.expect("run ok").expect("Some output");
assert!(matches!(output.result, InstructionResult::Return));
assert_eq!(output.gas.spent(), GAS_COST);
assert_eq!(output.gas.limit(), forwarded_gas);
assert_eq!(output.gas.remaining(), forwarded_gas - GAS_COST);
assert_eq!(context.additional_limit.borrow().get_usage().compute_gas, GAS_COST,);
}
#[test]
fn test_kzg_precompile_rex5_compute_gas_cap_oogs() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::REX5);
let compute_gas_limit = GAS_COST - 1;
set_tx_compute_gas_limit(&mut context, MegaSpecId::REX5, compute_gas_limit);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::REX5).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let result = precompiles_map.run(&mut context, &address, &inputs, true, 1_000_000);
let output = result.expect("run ok").expect("Some output");
assert!(matches!(output.result, InstructionResult::PrecompileOOG));
assert_eq!(output.gas.spent(), 0);
assert_eq!(context.additional_limit.borrow().get_usage().compute_gas, compute_gas_limit);
}
#[test]
fn test_kzg_precompile_rex5_no_cap_when_remaining_sufficient() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::REX5);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::REX5).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let forwarded_gas = 200_000u64;
let result = precompiles_map.run(&mut context, &address, &inputs, true, forwarded_gas);
let output = result.expect("run ok").expect("Some output");
assert!(matches!(output.result, InstructionResult::Return));
assert_eq!(output.gas.spent(), GAS_COST);
assert_eq!(output.gas.limit(), forwarded_gas);
assert_eq!(output.gas.remaining(), forwarded_gas - GAS_COST);
}
#[test]
fn test_kzg_precompile_rex5_cap_at_exact_gas_cost() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::REX5);
set_tx_compute_gas_limit(&mut context, MegaSpecId::REX5, GAS_COST);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::REX5).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let forwarded_gas = 500_000u64;
let result = precompiles_map.run(&mut context, &address, &inputs, true, forwarded_gas);
let output = result.expect("run ok").expect("Some output");
assert!(matches!(output.result, InstructionResult::Return));
assert_eq!(output.gas.spent(), GAS_COST);
assert_eq!(output.gas.limit(), forwarded_gas);
assert_eq!(output.gas.remaining(), forwarded_gas - GAS_COST);
assert_eq!(context.additional_limit.borrow().get_usage().compute_gas, GAS_COST);
}
#[test]
fn test_kzg_precompile_rex5_cap_at_exact_gas_cost_fail() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::REX5);
set_tx_compute_gas_limit(&mut context, MegaSpecId::REX5, GAS_COST);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::REX5).precompiles(),
);
let inputs = generate_invalid_proof_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let forwarded_gas = 500_000u64;
let result = precompiles_map.run(&mut context, &address, &inputs, true, forwarded_gas);
let output = result.expect("run ok").expect("Some output");
assert!(
matches!(output.result, InstructionResult::PrecompileError),
"expected PrecompileError on invalid-proof at the cap boundary; got {:?}",
output.result
);
assert_eq!(output.gas.limit(), GAS_COST);
assert_eq!(output.gas.spent(), 0);
assert_eq!(
context.additional_limit.borrow().get_usage().compute_gas,
GAS_COST,
"at the exact-cap boundary on a verification failure, the recorded \
compute-gas must equal GAS_COST"
);
}
#[test]
fn test_kzg_precompile_rex5_oog_via_cap_burns_full_tx_gas() {
use crate::{MegaEvm, MegaTransaction};
use alloy_primitives::{address, Bytes as BytesT, U256};
use revm::context::{tx::TxEnvBuilder, BlockEnv, ContextSetters};
let caller = address!("0000000000000000000000000000000000600000");
let mut db = MemoryDatabase::default().account_balance(caller, U256::from(10_000_000));
let mut context = MegaContext::new(&mut db, MegaSpecId::REX5);
let tx_limits = EvmTxRuntimeLimits {
tx_compute_gas_limit: GAS_COST - 1,
..EvmTxRuntimeLimits::from_spec(MegaSpecId::REX5)
};
context.additional_limit =
Rc::new(RefCell::new(AdditionalLimit::new(MegaSpecId::REX5, tx_limits)));
context.set_block(BlockEnv { gas_limit: 1_000_000_000, ..Default::default() });
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let valid_kzg_input = match generate_kzg_test_input().input {
revm::interpreter::CallInput::Bytes(b) => b,
_ => panic!("expected Bytes"),
};
let tx_gas_limit = 1_000_000u64;
let tx = TxEnvBuilder::default()
.caller(caller)
.call(revm::precompile::kzg_point_evaluation::ADDRESS)
.gas_limit(tx_gas_limit)
.gas_price(1)
.data(valid_kzg_input)
.build_fill();
let mut evm = MegaEvm::new(context);
let mut tx = MegaTransaction::new(tx);
tx.enveloped_tx = Some(BytesT::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transact ok");
assert!(!result.result.is_success(), "tx must halt: cap forced precompile OOG",);
assert_eq!(
result.result.gas_used(),
tx_gas_limit,
"OOG'd precompile call must burn the full tx.gas_limit (normalization must NOT \
leak a refund through the halt path)",
);
}
#[test]
fn test_kzg_precompile_pre_rex5_does_not_cap() {
let mut db = MemoryDatabase::default();
let mut context = MegaContext::new(&mut db, MegaSpecId::REX4);
set_tx_compute_gas_limit(&mut context, MegaSpecId::REX4, GAS_COST - 1);
let mut precompiles_map = PrecompilesMap::from_static(
MegaPrecompiles::new_with_spec(MegaSpecId::REX4).precompiles(),
);
let inputs = generate_kzg_test_input();
let address = revm::precompile::kzg_point_evaluation::ADDRESS;
let forwarded_gas = 500_000u64;
let result = precompiles_map.run(&mut context, &address, &inputs, true, forwarded_gas);
let output = result.expect("run ok").expect("Some output");
assert!(matches!(output.result, InstructionResult::Return));
assert_eq!(output.gas.spent(), GAS_COST);
assert_eq!(output.gas.limit(), forwarded_gas);
}
}