In this tutorial, we will walk through how to create your own precompile and add it to 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.
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{},
}
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);
}
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
}
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)
}
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
}
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{},
}
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"
- Open Remix.
- Connect to your local Geth network by selecting Custom - External Http Provider and using the default
http://127.0.0.1:8545
endpoint. - 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!
