diff --git a/core/stateless/encoding.go b/core/stateless/encoding.go index 5f4cb0ea3c..b5174ab586 100644 --- a/core/stateless/encoding.go +++ b/core/stateless/encoding.go @@ -19,7 +19,9 @@ package stateless import ( "io" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" ) @@ -74,3 +76,33 @@ type extWitness struct { Codes [][]byte State [][]byte } + +// ExecutionWitness is a witness json encoding for transferring across clients +// in the future, we'll probably consider using the extWitness format instead for less overhead. +// currently we're using this format for compatibility with reth and also for simplicity in terms of parsing. +type ExecutionWitness struct { + Headers []*types.Header `json:"headers"` + Codes map[string]string `json:"codes"` + State map[string]string `json:"state"` +} + +func transformMap(in map[string]struct{}) map[string]string { + out := make(map[string]string, len(in)) + for item := range in { + bytes := []byte(item) + key := crypto.Keccak256Hash(bytes).Hex() + out[key] = hexutil.Encode(bytes) + } + return out +} + +// ToExecutionWitness converts a witness to an execution witness format that is compatible with reth. +// keccak(node) => node +// keccak(bytecodes) => bytecodes +func (w *Witness) ToExecutionWitness() *ExecutionWitness { + return &ExecutionWitness{ + Headers: w.Headers, + Codes: transformMap(w.Codes), + State: transformMap(w.State), + } +} diff --git a/eth/api_debug.go b/eth/api_debug.go index d5e4dda140..dce5bbf1ba 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -24,8 +24,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/ethapi" @@ -443,3 +445,43 @@ func (api *DebugAPI) GetTrieFlushInterval() (string, error) { } return api.eth.blockchain.GetTrieFlushInterval().String(), nil } + +func (api *DebugAPI) ExecutionWitness(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*stateless.ExecutionWitness, error) { + block, err := api.eth.APIBackend.BlockByNumberOrHash(ctx, blockNrOrHash) + if err != nil { + return nil, fmt.Errorf("failed to retrieve block: %w", err) + } + if block == nil { + return nil, fmt.Errorf("block not found: %s", blockNrOrHash.String()) + } + + witness, err := generateWitness(api.eth.blockchain, block) + return witness.ToExecutionWitness(), err +} + +func generateWitness(blockchain *core.BlockChain, block *types.Block) (*stateless.Witness, error) { + witness, err := stateless.NewWitness(block.Header(), blockchain) + if err != nil { + return nil, fmt.Errorf("failed to create witness: %w", err) + } + + parentHeader := witness.Headers[0] + statedb, err := blockchain.StateAt(parentHeader.Root) + if err != nil { + return nil, fmt.Errorf("failed to retrieve parent state: %w", err) + } + + statedb.StartPrefetcher("debug_execution_witness", witness) + defer statedb.StopPrefetcher() + + res, err := blockchain.Processor().Process(block, statedb, *blockchain.GetVMConfig()) + if err != nil { + return nil, fmt.Errorf("failed to process block %d: %w", block.Number(), err) + } + + if err := blockchain.Validator().ValidateState(block, statedb, res, false); err != nil { + return nil, fmt.Errorf("failed to validate block %d: %w", block.Number(), err) + } + + return witness, nil +} diff --git a/eth/api_debug_test.go b/eth/api_debug_test.go index cfb8829b5c..c398680b14 100644 --- a/eth/api_debug_test.go +++ b/eth/api_debug_test.go @@ -19,6 +19,7 @@ package eth import ( "bytes" "fmt" + "math/big" "reflect" "slices" "strings" @@ -26,13 +27,18 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/triedb" "github.com/holiman/uint256" + "github.com/stretchr/testify/require" ) var dumper = spew.ConfigState{Indent: " "} @@ -224,3 +230,30 @@ func TestStorageRangeAt(t *testing.T) { } } } + +func TestExecutionWitness(t *testing.T) { + t.Parallel() + + // Create a database pre-initialize with a genesis block + db := rawdb.NewMemoryDatabase() + gspec := &core.Genesis{ + Config: params.TestChainConfig, + Alloc: types.GenesisAlloc{testAddr: {Balance: big.NewInt(1000000)}}, + } + chain, _ := core.NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil) + + blockNum := 10 + _, bs, _ := core.GenerateChainWithGenesis(gspec, ethash.NewFaker(), blockNum, nil) + if _, err := chain.InsertChain(bs); err != nil { + panic(err) + } + + block := chain.GetBlockByNumber(uint64(blockNum - 1)) + require.NotNil(t, block) + + witness, err := generateWitness(chain, block) + require.NoError(t, err) + + _, _, err = core.ExecuteStateless(params.TestChainConfig, block, witness) + require.NoError(t, err) +}