<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.

Repo

Here you can find P256VERIFY implemented on Revm:

GitHub - alessandromazza98/revm at eip-7212

Where?

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.

Structure of a Precompile

You only need to add 2 things:

  1. A new PrecompileAddress which is a tuple struct made of

    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.

  2. 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.

Configure EVM to include your newly added Precompile

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,
}