ethereum precompiles illustration

The Ethereum Virtual Machine is the runtime environment in which smart contracts are executed on this blockchain.
Before a smart contract can be executed, it needs to be compiled from its high-level Solidity (or Vyper if you are more a pythonesque person) language into bytecode, which is the machine-readable language of the EVM.
 
The process of this compilation is a follows:

  • Lexical analysis.
  • Parsing.
  • Semantic analysis.
  • Code generation.

The end result of this compiling process is a binary file containing the bytecode of the smart contract.

After understanding the compilation process transforming smart contracts into EVM bytecode, it is worth mentioning another important aspect of the Ethereum ecosystem: precompiles.

Precompiles contracts are part of the internal EVM and are just exposed in a way that Solidity can execute them. Precompiles are bundled in the EVM at specific, fixed addresses & called with determined gas cost. The purpose of those pre-deployed contracts is to provide certain complex operations that are usually too
computationally expensive to be executed on-chain like a regular soldity file would do or require special algorithms.
 

There exists 9 precompiles in the EVM and most of them are related to cryptographic primitives.
Among those 9 , I will just showcase 4 of them for brevity:

  • ECRecover: address 0x1 This pre-compile is used to recover the public key from a signature and a message. It is used in verifying signatures.

  • SHA256: address 0x2 This pre-compile is used for computing the SHA-256 hash of a given input.

  • RIPEMD160: address 0x3 This pre-compile is used for computing the RIPEMD-160 hash of a given input.

  • Identity: address 0x4 This is a simple pre-compile that returns the input unchanged.
    It is used as a no-op and can be seen as a basic cryptographic operation.

The advantage of precompiles is that you can code them in golang and render them way cheaper to interact with than if you were to code them directly in Solidity .

The ECRecover pre-compile in Ethereum uses the Elliptic Curve Digital Signature Algorithm (ECDSA) for signature verification. ECDSA is a widely used cryptographic signature scheme that is based on the mathematical properties of elliptic curves over finite fields.

In the context of Ethereum:

Message Hash:
Before signing, the message is first hashed using the Keccak-256 hash function.

Signing Process:
The private key holder generates the signature (v, r, s) using the ECDSA algorithm:

  • v: Recovery id (0 or 1) - Used to determine the correct public key among the two possibilities generated during the ECDSA signature process.

  • r: The x-coordinate of the point resulting from multiplying the private key with the base point on the elliptic curve.

  • s: The signature component derived from the private key, the message hash, and the x-coordinate of the resulting point.

Signature Format:
The signature (v, r, s) is usually represented as a 65-byte array in Ethereum:

  • v: 1 byte (0 or 1)
  • r: 32 bytes
  • s: 32 bytes

Using ECRecover for signature verification:

When using ECRecover to verify a signature in Ethereum, you need to provide:

  • The message hash (bytes32)
  • The recovery id v (uint8)
  • The r and s components of the signature (bytes32 each)

Here’s a simplified overview of the ECDSA signing and verification process:

1- Private Key: A random private key is generated.
2- Public Key: The corresponding public key is computed by multiplying the elliptic curve’s base point with the private key.
3- Message Hashing: The message to be signed is hashed using the Keccak-256 algorithm to produce a fixed-size hash.
4- Signature Generation: The signature is generated using the private key and the message hash.
 

Here’s the Solidity function to verify a signature using ECRecover:

function verifySignature(bytes32 message, uint8 v, bytes32 r, bytes32 s, address expectedSigner) public pure returns (bool) {
    bytes32 prefixedHash = keccak256(abi.encodePacked("hex\x19Ethereum Signed Message:\n32", message));
    address signer = ecrecover(prefixedHash, v, r, s);
    return signer == expectedSigner;
}

 

Below is an implementation of the ECrecover precompile in Go:


package main

import (
	
	"bytes"
	"crypto/ecdsa"
	"fmt"
	"math/big"
	"os"
	"github.com/ethereum/go-ethereum/common/math"
	"github.com/ethereum/go-ethereum/crypto"
)

// Constants for Ethereum elliptic curve parameters
var (
	secp256k1N, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
 
)

// Perform ecrecover on the provided message, signature, and v value
func ecrecover(message, signature, v []byte) (*ecdsa.PublicKey, error) {
	if len(signature) != 64 {
		return nil, fmt.Errorf("invalid signature length")
	}

	r := new(big.Int).SetBytes(signature[:32])
	s := new(big.Int).SetBytes(signature[32:])
	vVal := new(big.Int).SetBytes(v) 

	// Ensure the signature values are within the curve order (secp256k1N)
	if r.Cmp(secp256k1N) >= 0 || s.Cmp(secp256k1N) >= 0 {
		return nil, fmt.Errorf("invalid signature values")
	}

	// Prepare the hash for the recovery operation
	hash := prepareHash(message)




		// Recover the public key
		pubKey, err := crypto.SigToPub(hash, signature, vVal[0])
		if err != nil {
					return nil, fmt.Errorf("failed to recover public key!")
		}

		// Calculate the recovered address
		recoveredAddress := crypto.PubkeyToAddress(*pubKey).Hex()
		fmt.Printf("Recovered Address: %s\n", recoveredAddress)
		return pubKey, ni 
	}



// Prepare the hash by adding the necessary prefix
func prepareHash(message []byte) []byte {
	prefix := "\x19Ethereum Signed Message:\n"
	
	buf := new(bytes.Buffer)
	buf.WriteString(prefix)
	buf.WriteString(fmt.Sprintf("%d", len(message)))
	buf.WriteRune('\n')
	buf.Write(message)

	return buf.Bytes()
}

func main() {
	// Check if all input arguments are provided
	if len(os.Args) != 4 {
		fmt.Println("Usage: go run main.go <message> <signature> <v>")
		return
	}

	// Read the input arguments from CLI
	message := []byte(os.Args[1])
	signature := []byte(os.Args[2])
	v := []byte(os.Args[3])

	_, err := ecrecover(message, signature, v)
	if err != nil {
		fmt.Printf("Error: %s\n", err.Error())
	}
}

 

Stateful precompiles

Some engineers from the Avalabs team created a way to customize the EVM.

Precompiles in the Avalanche Subnet-EVM are different from the ones in Go-Ethereum in the sense that they can modify the state of the virtual machine - allowing for more customization and this potentially
revolutionary technology took the name of ‘stateful precompiles’ .

These stateful precompiles offers web3 programmers a new interface in order to add functionality to their EVM instance without writing a single line of solidity.

As such , this is going to be really interesting for everybody that wants to have their own instance of the subnet EVM if they want to have some level of customization. Because, once you have that, you can do
various state operations and do them entirely in golang.

The first stateful precompile to have been introduced by the Ava Labs team is an Allow List, which is basically a list saying ‘does this person , does this address have rightful access to this ressource ?’ .

These stateful precompiles can modify balances, nonces, accounts storage, unlocking a lot more potentiality for customization of your own EVM blockchain instance.

For a quick intro into stateful precompiles, check this talk by former AvaLabs engineer @AnushaChillara

If you wish to learn more about this way of building a EVM tailored to your specific needs, the free Avalanche Academy is the place to be : get certified