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()))
+ }
+}