Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save denniswon/4ae14a0706179c5d497937c5c2b1b785 to your computer and use it in GitHub Desktop.
Save denniswon/4ae14a0706179c5d497937c5c2b1b785 to your computer and use it in GitHub Desktop.

How to Add a Precompile to Geth

In this tutorial, we will walk through how to create your own precompile and add it to Geth.

What Is a Precompile in Geth?

In Geth, precompiles are a set of stateless contracts. But unlike Solidity code, their logic isn't in the smart contract code itself—they are part of the Geth client. You can think of a precompile as an interface that can execute Go code that resides within the Geth client.

How Precompiles Are Implemented in Geth

The logic for precompiles in Geth is located in the core/vm/contracts.go file. All precompile implementations must follow the PrecompiledContract interface.

type PrecompiledContract interface {
	RequiredGas(input []byte) uint64  // RequiredPrice calculates the contract gas use
	Run(input []byte) ([]byte, error) // Run runs the precompiled contract
}

Precompiles for the Cancun update are registered here:

// PrecompiledContractsCancun contains the default set of pre-compiled Ethereum
// contracts used in the Cancun release.
var PrecompiledContractsCancun = PrecompiledContracts{
	common.BytesToAddress([]byte{0x1}): &ecrecover{},
	common.BytesToAddress([]byte{0x2}): &sha256hash{},
	common.BytesToAddress([]byte{0x3}): &ripemd160hash{},
	common.BytesToAddress([]byte{0x4}): &dataCopy{},
	common.BytesToAddress([]byte{0x5}): &bigModExp{eip2565: true},
	common.BytesToAddress([]byte{0x6}): &bn256AddIstanbul{},
	common.BytesToAddress([]byte{0x7}): &bn256ScalarMulIstanbul{},
	common.BytesToAddress([]byte{0x8}): &bn256PairingIstanbul{},
	common.BytesToAddress([]byte{0x9}): &blake2F{},
	common.BytesToAddress([]byte{0xa}): &kzgPointEvaluation{},
}

Implement a Simple Add Precompile

In this tutorial, we will implement a simple precompile that adds two numbers:

interface AddPrecompile {
    function add(uint a, uint b) external view returns (uint result);
}

Step 1: Create new contracts_add.go file

Create a contracts_add.go file under the core/vm folder. This file will contain the logic for our precompile.

/*
`contracts_add.go` file implements a simple add precompile

interface AddPrecompile {
    function add(uint a, uint b) external view returns (uint result);
}
*/

package vm

// addContractAddr defines the precompile contract address for `add` precompile
var addContractAddr = common.HexToAddress("0x0100000000000000000000000000000000000001")
// add implements the PrecompiledContract interface
type add struct{}

// RequiredGas returns the gas needed to execute the precompile function.
//
// It implements the PrecompiledContract.RequiredGas method.
func (p *add) RequiredGas(input []byte) uint64 {
	return uint64(1024)
}

// Run contains the logic of the `add` precompile
//
// It implements the PrecompiledContract.Run method.
func (p *add) Run(input []byte) ([]byte, error) {
	// todo
	return nil, nil
}

Step 2: Unpack Input and Pack Output

In the Run method, the input is ABI-encoded, where the first 4 bytes represent the function selector. We need to unpack the input to get the uint a and uint b. The output should also be ABI-encoded.

var addABI = `[
			{
				"inputs": [
					{
						"internalType": "uint256",
						"name": "a",
						"type": "uint256"
					},
					{
						"internalType": "uint256",
						"name": "b",
						"type": "uint256"
					}
				],
				"name": "add",
				"outputs": [
					{
						"internalType": "uint256",
						"name": "result",
						"type": "uint256"
					}
				],
				"stateMutability": "view",
				"type": "function"
			}
		]`

// parseABI parses the abijson string and returns the parsed abi object.
func parseABI(abiJSON string) abi.ABI {
	parsed, err := abi.JSON(strings.NewReader(abiJSON))
	if err != nil {
		panic(err)
	}

	return parsed
}

// unpackAddInput unpacks the abi encoded input.
func unpackAddInput(input []byte) (*big.Int, *big.Int, error) {
	parsedABI := parseABI(addABI)
	functionInput := input[4:] // exclude function selector

	res, err := parsedABI.Methods["add"].Inputs.Unpack(functionInput)
	if err != nil {
		return nil, nil, err
	}

	if len(res) != 2 {
		return nil, nil, fmt.Errorf("invalid input for function add")
	}

	// convert to the actual function input type
	a := abi.ConvertType(res[0], new(big.Int)).(*big.Int)
	b := abi.ConvertType(res[1], new(big.Int)).(*big.Int)

	return a, b, nil
}

// packAddOutput pack the function result into output []byte
func packAddOutput(result *big.Int) ([]byte, error) {
	parsedABI := parseABI(addABI)
	return parsedABI.Methods["add"].Outputs.Pack(result)
}

Step 3: Implement the Core Logic in the Run Method

Now let’s complete the Run method with the core addition logic.

// Run contains the logic of the `add` precompile
//
// It implements the PrecompiledContract.Run method.
func (p *add) Run(input []byte) ([]byte, error) {
	if len(input) < 4 {
		return nil, fmt.Errorf("missing function selector")
	}

	// unpack input
	a, b, err := unpackAddInput(input)
	if err != nil {
		return nil, err
	}

	// calculate result of a + b
	result := new(big.Int).Add(a, b)

	// pack result into output
	output, err := packAddOutput(result)
	if err != nil {
		return nil, err
	}

	return output, nil
}

Step 4: Register the Precompile

Next, register the new precompile along with the others in the Cancun release. Open core/vm/contracts.go and add your precompile:

// PrecompiledContractsCancun contains the default set of pre-compiled Ethereum
// contracts used in the Cancun release.
var PrecompiledContractsCancun = map[common.Address]PrecompiledContract{
	common.BytesToAddress([]byte{0x1}): &ecrecover{},
	common.BytesToAddress([]byte{0x2}): &sha256hash{},
	common.BytesToAddress([]byte{0x3}): &ripemd160hash{},
	common.BytesToAddress([]byte{0x4}): &dataCopy{},
	common.BytesToAddress([]byte{0x5}): &bigModExp{eip2565: true},
	common.BytesToAddress([]byte{0x6}): &bn256AddIstanbul{},
	common.BytesToAddress([]byte{0x7}): &bn256ScalarMulIstanbul{},
	common.BytesToAddress([]byte{0x8}): &bn256PairingIstanbul{},
	common.BytesToAddress([]byte{0x9}): &blake2F{},
	common.BytesToAddress([]byte{0xa}): &kzgPointEvaluation{},

	// register the `add`` precompile
	addContractAddr: &add{},
}

Test the precomile

Step 1: Run Geth in Development Mode

To test the modified Geth with the new precompile, you can run Geth in development mode by adding this command to the Makefile and running make devnet:

devnet:
	go run ./cmd/geth/ --dev --http --http.addr "0.0.0.0" --http.port 8545 --http.api "personal,eth,net,web3,admin,debug" --allow-insecure-unlock console -cache.preimages=true --http.corsdomain "https://remix.ethereum.org"

You can also build the modified Geth with make geth and run: ./build/bin/geth --dev --http --http.addr "0.0.0.0" --http.port 8545 --http.api "personal,eth,net,web3,admin,debug" --allow-insecure-unlock console -cache.preimages=true --http.corsdomain "https://remix.ethereum.org"

Test the Precompile with Remix

  1. Open Remix.
  2. Connect to your local Geth network by selecting Custom - External Http Provider and using the default http://127.0.0.1:8545 endpoint.
  3. Interact with the precompile at the address 0x0100000000000000000000000000000000000001 using the following Solidity interface:
interface AddPrecompile {
    function add(uint a, uint b) external view returns (uint result);
}

Call the add function and test it!

image
/*
This file implements a simple add precompile
interface AddPrecompile {
function add(uint a, uint b) external view returns (uint result);
}
*/
package vm
import (
"fmt"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
// addContractAddr defines the precompile contract address for `add` precompile
var addContractAddr = common.HexToAddress("0x0100000000000000000000000000000000000001")
// add implements the PrecompiledContract interface
type add struct{}
// RequiredGas returns the gas needed to execute the precompile function.
//
// It implements the PrecompiledContract.RequiredGas method.
func (p *add) RequiredGas(input []byte) uint64 {
return uint64(1024)
}
// Run contains the logic of the `add` precompile
//
// It implements the PrecompiledContract.Run method.
func (p *add) Run(input []byte) ([]byte, error) {
if len(input) < 4 {
return nil, fmt.Errorf("missing function selector")
}
// unpack input
a, b, err := unpackAddInput(input)
if err != nil {
return nil, err
}
// calculate result of a + b
result := new(big.Int).Add(a, b)
// pack result into output
output, err := packAddOutput(result)
if err != nil {
return nil, err
}
return output, nil
}
var addABI = `[
{
"inputs": [
{
"internalType": "uint256",
"name": "a",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "b",
"type": "uint256"
}
],
"name": "add",
"outputs": [
{
"internalType": "uint256",
"name": "result",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]`
// parseABI parses the abijson string and returns the parsed abi object.
func parseABI(abiJSON string) abi.ABI {
parsed, err := abi.JSON(strings.NewReader(abiJSON))
if err != nil {
panic(err)
}
return parsed
}
// unpackAddInput unpacks the abi encoded input.
func unpackAddInput(input []byte) (*big.Int, *big.Int, error) {
parsedABI := parseABI(addABI)
functionInput := input[4:] // exclude function selector
res, err := parsedABI.Methods["add"].Inputs.Unpack(functionInput)
if err != nil {
return nil, nil, err
}
if len(res) != 2 {
return nil, nil, fmt.Errorf("invalid input for function add")
}
// convert to the actual function input type
a := abi.ConvertType(res[0], new(big.Int)).(*big.Int)
b := abi.ConvertType(res[1], new(big.Int)).(*big.Int)
return a, b, nil
}
// packAddOutput pack the function result into output []byte
func packAddOutput(result *big.Int) ([]byte, error) {
parsedABI := parseABI(addABI)
return parsedABI.Methods["add"].Outputs.Pack(result)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment