Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to Add a Precompile to Geth #8

Open
zt-9 opened this issue Sep 20, 2024 · 0 comments
Open

How to Add a Precompile to Geth #8

zt-9 opened this issue Sep 20, 2024 · 0 comments

Comments

@zt-9
Copy link
Owner

zt-9 commented Sep 20, 2024

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

You can check out the full code in this gist.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant