<aside>
💡 In this article I’ll explain the process of adding a new precompile inside Revm
.
</aside>
Adding a new Precompile inside Revm
is quite straight forward. I’ll explain my story of adding P256VERIFY
precompile.
Here you can find P256VERIFY
implemented on Revm
:
GitHub - alessandromazza98/revm at eip-7212
Precompiles are in *crates/precompiles/src
.*
So the first step is to add a new file inside this folder: this is the place where you have to put whole the code for your precompile logic.
You only need to add 2 things:
A new PrecompileAddress
which is a tuple struct made of
an Address
: just a B160
a Precompile
: an enum that has 2 variants:
Standard(StandardPrecompileFn)
Env(EnvPrecompileFn)
where:
StandardPrecompileFn
is a fn(&[u8], u64) -> PrecompileResult
.
Inputs of that function are:
&[u8]
: input data for the precompileu64
: actual gas that is given to the precompileA PrecompileResult
is a Result<(u64, Vec<u8>), PrecompileError>
and represents the whole result of a precompile operation, that is:
u64
, actual result data of the precompile operation: Vec<u8>
)PrecompileError
is just an enum that collects all possible errors you may encounter during the execution of a precompile. If your precompile could throw specific errors you may have to add some variants to PrecompileError
variant so that you are then able to return the most appropriate one when it’s needed.EnvPrecompileFn
is a fn(&[u8], u64, env: &Env) -> PrecompileResult
. It’s almost identical to the Standard
one with the only difference that it takes an additional &Env
parameter.
The following is the standard way in Revm
to create a new PrecompileAddress
:
pub const P256VERIFY: PrecompileAddress = PrecompileAddress(
// be careful to select a non-already-used address
crate::u64_to_b160(10),
Precompile::Standard(p256_verify as StandardPrecompileFn),
);
CAUTION: I chose address number 10, which then gets translated into 0x00...0a
because, as of now, Revm
works using the assumption that precompile addresses are in a sequence and there are no wholes in between. So, since there were only 9 precompiles, I chose address 10. But this behaviour will probably change in the future.
The actual Standard
or Env
precompile function. In my example p256_verify
.
fn p256_verify(i: &[u8], target_gas: u64) -> PrecompileResult {
use core::cmp::min;
use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey};
const P256VERIFY_BASE: u64 = 3_450;
if P256VERIFY_BASE > target_gas {
return Err(Error::OutOfGas);
}
let mut input = [0u8; 160];
input[..min(i.len(), 160)].copy_from_slice(&i[..min(i.len(), 160)]);
// msg signed (msg is already the hash of the original message)
let msg: [u8; 32] = input[..32].try_into().unwrap();
// r, s: signature
let sig: [u8; 64] = input[32..96].try_into().unwrap();
// x, y: public key
let pk: [u8; 64] = input[96..160].try_into().unwrap();
// append 0x04 to the public key: uncompressed form
let mut uncompressed_pk = [0u8; 65];
uncompressed_pk[0] = 0x04;
uncompressed_pk[1..].copy_from_slice(&pk);
let signature: Signature = Signature::from_slice(&sig).unwrap();
let public_key: VerifyingKey = VerifyingKey::from_sec1_bytes(&uncompressed_pk).unwrap();
let mut result = [0u8; 32];
// verify
if public_key.verify_prehash(&msg, &signature).is_ok() {
result[31] = 0x01;
Ok((P256VERIFY_BASE, result.into()))
} else {
Ok((P256VERIFY_BASE, result.into()))
}
}
I used p256
crate for elliptic curve operations here. Note that it has never been independently audited, as mentioned in the docs.
To understand better P256VERIFY
precompile I suggest you to read the original EIP! Its authors have implemented it only on Geth. That is my implementation for Revm
, which is then used inside Reth.
Now P256VERIFY
is implemented inside Revm
but if you don’t add it in a new (or even an old if you want) SpecId
, it will never be functioning in a real (or even test) environment.
This is because when transact()
is called, it calls evm_inner()
which creates an actual EVMImpl
with all Precompiles
built in, based on the SpecId
of the EVM.
In *crates/precompile/src/lib.rs
* you can add a new SpecId
variant for EVM precompiles:
pub enum SpecId {
HOMESTEAD,
BYZANTIUM,
ISTANBUL,
BERLIN,
ALESSANDRO, // this is the new one
LATEST,
}