From ac3f406ead652b0b324744231bf5220bc1c44d19 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 4 Sep 2025 17:23:31 +0200 Subject: [PATCH] cmd/keeper: add the keeper zkvm guest program --- cmd/keeper/README.md | 89 +++++++++++++++++++++++++++ cmd/keeper/getpayload_example.go | 101 +++++++++++++++++++++++++++++++ cmd/keeper/main.go | 52 ++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 cmd/keeper/README.md create mode 100644 cmd/keeper/getpayload_example.go create mode 100644 cmd/keeper/main.go diff --git a/cmd/keeper/README.md b/cmd/keeper/README.md new file mode 100644 index 0000000000..209977aeb6 --- /dev/null +++ b/cmd/keeper/README.md @@ -0,0 +1,89 @@ +# Keeper - geth as a zkvm guest + +Keeper command is a specialized tool for validating stateless execution of Ethereum blocks. It's designed to run as a zkvm guest. + +## Overview + +The keeper reads an RLP-encoded payload containing: +- A block to execute +- A witness with the necessary state data + +It then executes the block statelessly and validates that the computed state root and receipt root match the values in the block header. + +## Architecture + +The keeper uses build tags to compile platform-specific input methods: + +``` +cmd/keeper/ +├── main.go # Main execution logic +├── getpayload_example.go # Example implementation +└── README.md # This file +``` + +## Creating a Custom Platform Implementation + +To add support for a new platform (e.g., "myplatform"), create a new file with the appropriate build tag: + +### 1. Create `getinput_myplatform.go` + +```go +//go:build myplatform + +package main + +import ( + "github.com/ethereum/go-ethereum/params" + // ... other imports as needed +) + +// getChainConfig returns the chain configuration for this platform +func getChainConfig() *params.ChainConfig { + // Return the appropriate chain config for your platform + // Examples: params.MainnetChainConfig, params.SepoliaChainConfig, + // or a custom configuration + return params.MainnetChainConfig +} + +// getInput returns the RLP-encoded payload +func getInput() []byte { + // Your platform-specific code to retrieve the RLP-encoded payload + // This might read from: + // - Memory-mapped I/O + // - Hardware registers + // - Serial port + // - Network interface + // - File system + + // The payload must be RLP-encoded and contain: + // - Block with transactions + // - Witness with parent headers and state data + + return encodedPayload +} +``` + +### 2. Build for Your Platform + +```bash +go build -tags myplatform ./cmd/keeper +``` + +## Payload Structure + +The payload is an RLP-encoded structure containing: + +```go +type Payload struct { + Block *types.Block + Witness *stateless.Witness +} +``` + +## Example Implementation + +See `getinput_example.go` for a complete example that contains a payload. To build the example: + +```bash +go build -tags example ./cmd/keeper +``` diff --git a/cmd/keeper/getpayload_example.go b/cmd/keeper/getpayload_example.go new file mode 100644 index 0000000000..9af25baea9 --- /dev/null +++ b/cmd/keeper/getpayload_example.go @@ -0,0 +1,101 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +//go:build example + +package main + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/stateless" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" +) + +func getChainConfig() *params.ChainConfig { + return params.MainnetChainConfig +} + +func getInput() []byte { + header := &types.Header{ + ParentHash: common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"), + UncleHash: types.EmptyUncleHash, + Coinbase: common.HexToAddress("0x0000000000000000000000000000000000000000"), + Root: common.Hash{}, // Will be computed by stateless execution + TxHash: types.EmptyTxsHash, + ReceiptHash: types.EmptyReceiptsHash, + Bloom: types.Bloom{}, + Difficulty: big.NewInt(0), + Number: big.NewInt(20000000), // Post-merge block number + GasLimit: 30000000, + GasUsed: 0, + Time: 1700000000, // Recent timestamp + Extra: []byte("Example block for platform builders"), + MixDigest: common.Hash{}, + Nonce: types.BlockNonce{}, + BaseFee: big.NewInt(1000000000), // 1 gwei base fee + } + + // For this example, create an empty block (no transactions) + // A real implementation would include properly funded accounts + body := &types.Body{ + Transactions: []*types.Transaction{}, + } + block := types.NewBlock(header, body, nil, trie.NewStackTrie(nil)) + + // Create a parent header (required by witness) + parentHeader := &types.Header{ + ParentHash: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), + UncleHash: types.EmptyUncleHash, + Coinbase: common.HexToAddress("0x0000000000000000000000000000000000000000"), + Root: common.HexToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + TxHash: types.EmptyTxsHash, + ReceiptHash: types.EmptyReceiptsHash, + Bloom: types.Bloom{}, + Difficulty: big.NewInt(0), + Number: big.NewInt(19999999), + GasLimit: 30000000, + GasUsed: 0, + Time: 1699999999, + Extra: []byte("Parent block"), + MixDigest: common.Hash{}, + Nonce: types.BlockNonce{}, + BaseFee: big.NewInt(1000000000), + } + + // The witness needs state nodes for any accounts accessed + // For an empty block, minimal state is needed + witness := &stateless.Witness{ + Headers: []*types.Header{parentHeader}, + Codes: map[string]struct{}{}, + State: map[string]struct{}{}, + } + + payload := Payload{ + Block: block, + Witness: witness, + } + + encoded, err := rlp.EncodeToBytes(payload) + if err != nil { + panic(err) + } + return encoded +} diff --git a/cmd/keeper/main.go b/cmd/keeper/main.go new file mode 100644 index 0000000000..aeda579581 --- /dev/null +++ b/cmd/keeper/main.go @@ -0,0 +1,52 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package main + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/stateless" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/rlp" +) + +type Payload struct { + Block *types.Block + Witness *stateless.Witness +} + +func main() { + input := getPayload() + var payload Payload + rlp.DecodeBytes(input, &payload) + + chainConfig := getChainConfig() + vmConfig := vm.Config{} + + crossStateRoot, crossReceiptRoot, err := core.ExecuteStateless(chainConfig, vmConfig, payload.Block, payload.Witness) + if err != nil { + panic(fmt.Errorf("stateless self-validation failed: %v", err)) + } + if crossStateRoot != payload.Block.Root() { + panic(fmt.Errorf("stateless self-validation root mismatch (cross: %x local: %x)", crossStateRoot, payload.Block.Root())) + } + if crossReceiptRoot != payload.Block.ReceiptHash() { + panic(fmt.Errorf("stateless self-validation receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, payload.Block.ReceiptHash())) + } +}