From e376d2fb31a8fa131ab1f6b9f96ac8a41c94e007 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Tue, 30 Jun 2020 10:12:51 +0200 Subject: [PATCH] cmd/evm: add state transition tool for testing (#20958) This PR implements the EVM state transition tool, which is intended to be the replacement for our retesteth client implementation. Documentation is present in the cmd/evm/README.md file. Co-authored-by: Felix Lange --- cmd/evm/README.md | 268 ++++++++++++++++++++++++ cmd/evm/internal/t8ntool/execution.go | 255 +++++++++++++++++++++++ cmd/evm/internal/t8ntool/flags.go | 99 +++++++++ cmd/evm/internal/t8ntool/gen_stenv.go | 80 +++++++ cmd/evm/internal/t8ntool/transition.go | 276 +++++++++++++++++++++++++ cmd/evm/main.go | 29 ++- cmd/evm/poststate.json | 23 +++ cmd/evm/testdata/1/alloc.json | 12 ++ cmd/evm/testdata/1/env.json | 7 + cmd/evm/testdata/1/txs.json | 26 +++ cmd/evm/testdata/2/alloc.json | 16 ++ cmd/evm/testdata/2/env.json | 7 + cmd/evm/testdata/2/readme.md | 1 + cmd/evm/testdata/2/txs.json | 14 ++ cmd/evm/testdata/3/alloc.json | 16 ++ cmd/evm/testdata/3/env.json | 8 + cmd/evm/testdata/3/readme.md | 2 + cmd/evm/testdata/3/txs.json | 14 ++ cmd/evm/testdata/4/alloc.json | 16 ++ cmd/evm/testdata/4/env.json | 8 + cmd/evm/testdata/4/readme.md | 3 + cmd/evm/testdata/4/txs.json | 14 ++ cmd/evm/testdata/5/alloc.json | 1 + cmd/evm/testdata/5/env.json | 11 + cmd/evm/testdata/5/readme.md | 1 + cmd/evm/testdata/5/txs.json | 1 + cmd/evm/testdata/7/alloc.json | 12 ++ cmd/evm/testdata/7/env.json | 7 + cmd/evm/testdata/7/readme.md | 7 + cmd/evm/testdata/7/txs.json | 1 + cmd/evm/transition-test.sh | 191 +++++++++++++++++ cmd/utils/flags.go | 10 +- core/state/dump.go | 63 +++--- core/state/state_test.go | 4 +- core/vm/eips.go | 34 ++- tests/init.go | 11 + tests/state_test_util.go | 9 +- 37 files changed, 1509 insertions(+), 48 deletions(-) create mode 100644 cmd/evm/README.md create mode 100644 cmd/evm/internal/t8ntool/execution.go create mode 100644 cmd/evm/internal/t8ntool/flags.go create mode 100644 cmd/evm/internal/t8ntool/gen_stenv.go create mode 100644 cmd/evm/internal/t8ntool/transition.go create mode 100644 cmd/evm/poststate.json create mode 100644 cmd/evm/testdata/1/alloc.json create mode 100644 cmd/evm/testdata/1/env.json create mode 100644 cmd/evm/testdata/1/txs.json create mode 100644 cmd/evm/testdata/2/alloc.json create mode 100644 cmd/evm/testdata/2/env.json create mode 100644 cmd/evm/testdata/2/readme.md create mode 100644 cmd/evm/testdata/2/txs.json create mode 100644 cmd/evm/testdata/3/alloc.json create mode 100644 cmd/evm/testdata/3/env.json create mode 100644 cmd/evm/testdata/3/readme.md create mode 100644 cmd/evm/testdata/3/txs.json create mode 100644 cmd/evm/testdata/4/alloc.json create mode 100644 cmd/evm/testdata/4/env.json create mode 100644 cmd/evm/testdata/4/readme.md create mode 100644 cmd/evm/testdata/4/txs.json create mode 100644 cmd/evm/testdata/5/alloc.json create mode 100644 cmd/evm/testdata/5/env.json create mode 100644 cmd/evm/testdata/5/readme.md create mode 100644 cmd/evm/testdata/5/txs.json create mode 100644 cmd/evm/testdata/7/alloc.json create mode 100644 cmd/evm/testdata/7/env.json create mode 100644 cmd/evm/testdata/7/readme.md create mode 100644 cmd/evm/testdata/7/txs.json create mode 100644 cmd/evm/transition-test.sh diff --git a/cmd/evm/README.md b/cmd/evm/README.md new file mode 100644 index 000000000..418417475 --- /dev/null +++ b/cmd/evm/README.md @@ -0,0 +1,268 @@ +## EVM state transition tool + +The `evm t8n` tool is a stateless state transition utility. It is a utility +which can + +1. Take a prestate, including + - Accounts, + - Block context information, + - Previous blockshashes (*optional) +2. Apply a set of transactions, +3. Apply a mining-reward (*optional), +4. And generate a post-state, including + - State root, transaction root, receipt root, + - Information about rejected transactions, + - Optionally: a full or partial post-state dump + +## Specification + +The idea is to specify the behaviour of this binary very _strict_, so that other +node implementors can build replicas based on their own state-machines, and the +state generators can swap between a `geth`-based implementation and a `parityvm`-based +implementation. + +### Command line params + +Command line params that has to be supported are +``` + + --trace Output full trace logs to files .jsonl + --trace.nomemory Disable full memory dump in traces + --trace.nostack Disable stack output in traces + --output.alloc alloc Determines where to put the alloc of the post-state. + `stdout` - into the stdout output + `stderr` - into the stderr output + --output.result result Determines where to put the result (stateroot, txroot etc) of the post-state. + `stdout` - into the stdout output + `stderr` - into the stderr output + --state.fork value Name of ruleset to use. + --state.chainid value ChainID to use (default: 1) + --state.reward value Mining reward. Set to -1 to disable (default: 0) + +``` + +### Error codes and output + +All logging should happen against the `stderr`. +There are a few (not many) errors that can occur, those are defined below. + +#### EVM-based errors (`2` to `9`) + +- Other EVM error. Exit code `2` +- Failed configuration: when a non-supported or invalid fork was specified. Exit code `3`. +- Block history is not supplied, but needed for a `BLOCKHASH` operation. If `BLOCKHASH` + is invoked targeting a block which history has not been provided for, the program will + exit with code `4`. + +#### IO errors (`10`-`20`) + +- Invalid input json: the supplied data could not be marshalled. + The program will exit with code `10` +- IO problems: failure to load or save files, the program will exit with code `11` + +## Examples +### Basic usage + +Invoking it with the provided example files +``` +./evm t8n --input.alloc=./testdata/1/alloc.json --input.txs=./testdata/1/txs.json --input.env=./testdata/1/env.json +``` +Two resulting files: + +`alloc.json`: +```json +{ + "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192": { + "balance": "0xfeed1a9d", + "nonce": "0x1" + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x5ffd4878be161d74", + "nonce": "0xac" + }, + "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xa410" + } +} +``` +`result.json`: +```json +{ + "stateRoot": "0x84208a19bc2b46ada7445180c1db162be5b39b9abc8c0a54b05d32943eae4e13", + "txRoot": "0xc4761fd7b87ff2364c7c60b6c5c8d02e522e815328aaea3f20e3b7b7ef52c42d", + "receiptRoot": "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2", + "logsHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "receipts": [ + { + "root": "0x", + "status": "0x1", + "cumulativeGasUsed": "0x5208", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "logs": null, + "transactionHash": "0x0557bacce3375c98d806609b8d5043072f0b6a8bae45ae5a67a00d3a1a18d673", + "contractAddress": "0x0000000000000000000000000000000000000000", + "gasUsed": "0x5208", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionIndex": "0x0" + } + ], + "rejected": [ + 1 + ] +} +``` + +We can make them spit out the data to e.g. `stdout` like this: +``` +./evm t8n --input.alloc=./testdata/1/alloc.json --input.txs=./testdata/1/txs.json --input.env=./testdata/1/env.json --output.result=stdout --output.alloc=stdout +``` +Output: +```json +{ + "alloc": { + "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192": { + "balance": "0xfeed1a9d", + "nonce": "0x1" + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x5ffd4878be161d74", + "nonce": "0xac" + }, + "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xa410" + } + }, + "result": { + "stateRoot": "0x84208a19bc2b46ada7445180c1db162be5b39b9abc8c0a54b05d32943eae4e13", + "txRoot": "0xc4761fd7b87ff2364c7c60b6c5c8d02e522e815328aaea3f20e3b7b7ef52c42d", + "receiptRoot": "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2", + "logsHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "receipts": [ + { + "root": "0x", + "status": "0x1", + "cumulativeGasUsed": "0x5208", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "logs": null, + "transactionHash": "0x0557bacce3375c98d806609b8d5043072f0b6a8bae45ae5a67a00d3a1a18d673", + "contractAddress": "0x0000000000000000000000000000000000000000", + "gasUsed": "0x5208", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionIndex": "0x0" + } + ], + "rejected": [ + 1 + ] + } +} +``` + +## About Ommers + +Mining rewards and ommer rewards might need to be added. This is how those are applied: + +- `block_reward` is the block mining reward for the miner (`0xaa`), of a block at height `N`. +- For each ommer (mined by `0xbb`), with blocknumber `N-delta` + - (where `delta` is the difference between the current block and the ommer) + - The account `0xbb` (ommer miner) is awarded `(8-delta)/ 8 * block_reward` + - The account `0xaa` (block miner) is awarded `block_reward / 32` + +To make `state_t8n` apply these, the following inputs are required: + +- `state.reward` + - For ethash, it is `5000000000000000000` `wei`, + - If this is not defined, mining rewards are not applied, + - A value of `0` is valid, and causes accounts to be 'touched'. +- For each ommer, the tool needs to be given an `address` and a `delta`. This + is done via the `env`. + +Note: the tool does not verify that e.g. the normal uncle rules apply, +and allows e.g two uncles at the same height, or the uncle-distance. This means that +the tool allows for negative uncle reward (distance > 8) + +Example: +`./testdata/5/env.json`: +```json +{ + "currentCoinbase": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "currentDifficulty": "0x20000", + "currentGasLimit": "0x750a163df65e8a", + "currentNumber": "1", + "currentTimestamp": "1000", + "ommers": [ + {"delta": 1, "address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }, + {"delta": 2, "address": "0xcccccccccccccccccccccccccccccccccccccccc" } + ] +} +``` +When applying this, using a reward of `0x08` +Output: +```json +{ + "alloc": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "balance": "0x88" + }, + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "balance": "0x70" + }, + "0xcccccccccccccccccccccccccccccccccccccccc": { + "balance": "0x60" + } + } +} +``` +### Future EIPS + +It is also possible to experiment with future eips that are not yet defined in a hard fork. +Example, putting EIP-1344 into Frontier: +``` +./evm t8n --state.fork=Frontier+1344 --input.pre=./testdata/1/pre.json --input.txs=./testdata/1/txs.json --input.env=/testdata/1/env.json +``` + +### Block history + +The `BLOCKHASH` opcode requires blockhashes to be provided by the caller, inside the `env`. +If a required blockhash is not provided, the exit code should be `4`: +Example where blockhashes are provided: +``` +./evm t8n --input.alloc=./testdata/3/alloc.json --input.txs=./testdata/3/txs.json --input.env=./testdata/3/env.json --trace +``` +``` +cat trace-0.jsonl | grep BLOCKHASH -C2 +``` +``` +{"pc":0,"op":96,"gas":"0x5f58ef8","gasCost":"0x3","memory":"0x","memSize":0,"stack":[],"returnStack":[],"depth":1,"refund":0,"opName":"PUSH1","error":""} +{"pc":2,"op":64,"gas":"0x5f58ef5","gasCost":"0x14","memory":"0x","memSize":0,"stack":["0x1"],"returnStack":[],"depth":1,"refund":0,"opName":"BLOCKHASH","error":""} +{"pc":3,"op":0,"gas":"0x5f58ee1","gasCost":"0x0","memory":"0x","memSize":0,"stack":["0xdac58aa524e50956d0c0bae7f3f8bb9d35381365d07804dd5b48a5a297c06af4"],"returnStack":[],"depth":1,"refund":0,"opName":"STOP","error":""} +{"output":"","gasUsed":"0x17","time":155861} +``` + +In this example, the caller has not provided the required blockhash: +``` +./evm t8n --input.alloc=./testdata/4/alloc.json --input.txs=./testdata/4/txs.json --input.env=./testdata/4/env.json --trace +``` +``` +ERROR(4): getHash(3) invoked, blockhash for that block not provided +``` +Error code: 4 +### Chaining + +Another thing that can be done, is to chain invocations: +``` +./evm t8n --input.alloc=./testdata/1/alloc.json --input.txs=./testdata/1/txs.json --input.env=./testdata/1/env.json --output.alloc=stdout | ./evm t8n --input.alloc=stdin --input.env=./testdata/1/env.json --input.txs=./testdata/1/txs.json +INFO [06-29|11:52:04.934] rejected tx index=1 hash="0557ba…18d673" from=0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192 error="nonce too low" +INFO [06-29|11:52:04.936] rejected tx index=0 hash="0557ba…18d673" from=0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192 error="nonce too low" +INFO [06-29|11:52:04.936] rejected tx index=1 hash="0557ba…18d673" from=0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192 error="nonce too low" + +``` +What happened here, is that we first applied two identical transactions, so the second one was rejected. +Then, taking the poststate alloc as the input for the next state, we tried again to include +the same two transactions: this time, both failed due to too low nonce. + +In order to meaningfully chain invocations, one would need to provide meaningful new `env`, otherwise the +actual blocknumber (exposed to the EVM) would not increase. + diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go new file mode 100644 index 000000000..a4fa971eb --- /dev/null +++ b/cmd/evm/internal/t8ntool/execution.go @@ -0,0 +1,255 @@ +// Copyright 2020 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 t8ntool + +import ( + "fmt" + "math/big" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/consensus/misc" + "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/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "golang.org/x/crypto/sha3" +) + +type Prestate struct { + Env stEnv `json:"env"` + Pre core.GenesisAlloc `json:"pre"` +} + +// ExecutionResult contains the execution status after running a state test, any +// error that might have occurred and a dump of the final state if requested. +type ExecutionResult struct { + StateRoot common.Hash `json:"stateRoot"` + TxRoot common.Hash `json:"txRoot"` + ReceiptRoot common.Hash `json:"receiptRoot"` + LogsHash common.Hash `json:"logsHash"` + Bloom types.Bloom `json:"logsBloom" gencodec:"required"` + Receipts types.Receipts `json:"receipts"` + Rejected []int `json:"rejected,omitempty"` +} + +type ommer struct { + Delta uint64 `json:"delta"` + Address common.Address `json:"address"` +} + +//go:generate gencodec -type stEnv -field-override stEnvMarshaling -out gen_stenv.go +type stEnv struct { + Coinbase common.Address `json:"currentCoinbase" gencodec:"required"` + Difficulty *big.Int `json:"currentDifficulty" gencodec:"required"` + GasLimit uint64 `json:"currentGasLimit" gencodec:"required"` + Number uint64 `json:"currentNumber" gencodec:"required"` + Timestamp uint64 `json:"currentTimestamp" gencodec:"required"` + BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"` + Ommers []ommer `json:"ommers,omitempty"` +} + +type stEnvMarshaling struct { + Coinbase common.UnprefixedAddress + Difficulty *math.HexOrDecimal256 + GasLimit math.HexOrDecimal64 + Number math.HexOrDecimal64 + Timestamp math.HexOrDecimal64 +} + +// Apply applies a set of transactions to a pre-state +func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, + txs types.Transactions, miningReward int64, + getTracerFn func(txIndex int) (tracer vm.Tracer, err error)) (*state.StateDB, *ExecutionResult, error) { + + // Capture errors for BLOCKHASH operation, if we haven't been supplied the + // required blockhashes + var hashError error + getHash := func(num uint64) common.Hash { + if pre.Env.BlockHashes == nil { + hashError = fmt.Errorf("getHash(%d) invoked, no blockhashes provided", num) + return common.Hash{} + } + h, ok := pre.Env.BlockHashes[math.HexOrDecimal64(num)] + if !ok { + hashError = fmt.Errorf("getHash(%d) invoked, blockhash for that block not provided", num) + } + return h + } + var ( + statedb = MakePreState(rawdb.NewMemoryDatabase(), pre.Pre) + signer = types.MakeSigner(chainConfig, new(big.Int).SetUint64(pre.Env.Number)) + gaspool = new(core.GasPool) + blockHash = common.Hash{0x13, 0x37} + rejectedTxs []int + includedTxs types.Transactions + gasUsed = uint64(0) + receipts = make(types.Receipts, 0) + txIndex = 0 + ) + gaspool.AddGas(pre.Env.GasLimit) + vmContext := vm.Context{ + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + Coinbase: pre.Env.Coinbase, + BlockNumber: new(big.Int).SetUint64(pre.Env.Number), + Time: new(big.Int).SetUint64(pre.Env.Timestamp), + Difficulty: pre.Env.Difficulty, + GasLimit: pre.Env.GasLimit, + GetHash: getHash, + // GasPrice and Origin needs to be set per transaction + } + // If DAO is supported/enabled, we need to handle it here. In geth 'proper', it's + // done in StateProcessor.Process(block, ...), right before transactions are applied. + if chainConfig.DAOForkSupport && + chainConfig.DAOForkBlock != nil && + chainConfig.DAOForkBlock.Cmp(new(big.Int).SetUint64(pre.Env.Number)) == 0 { + misc.ApplyDAOHardFork(statedb) + } + + for i, tx := range txs { + msg, err := tx.AsMessage(signer) + if err != nil { + log.Info("rejected tx", "index", i, "hash", tx.Hash(), "error", err) + rejectedTxs = append(rejectedTxs, i) + continue + } + tracer, err := getTracerFn(txIndex) + if err != nil { + return nil, nil, err + } + vmConfig.Tracer = tracer + vmConfig.Debug = (tracer != nil) + statedb.Prepare(tx.Hash(), blockHash, txIndex) + vmContext.GasPrice = msg.GasPrice() + vmContext.Origin = msg.From() + + evm := vm.NewEVM(vmContext, statedb, chainConfig, vmConfig) + snapshot := statedb.Snapshot() + // (ret []byte, usedGas uint64, failed bool, err error) + msgResult, err := core.ApplyMessage(evm, msg, gaspool) + if err != nil { + statedb.RevertToSnapshot(snapshot) + log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From(), "error", err) + rejectedTxs = append(rejectedTxs, i) + continue + } + includedTxs = append(includedTxs, tx) + if hashError != nil { + return nil, nil, NewError(ErrorMissingBlockhash, hashError) + } + gasUsed += msgResult.UsedGas + // Create a new receipt for the transaction, storing the intermediate root and gas used by the tx + { + var root []byte + if chainConfig.IsByzantium(vmContext.BlockNumber) { + statedb.Finalise(true) + } else { + root = statedb.IntermediateRoot(chainConfig.IsEIP158(vmContext.BlockNumber)).Bytes() + } + + receipt := types.NewReceipt(root, msgResult.Failed(), gasUsed) + receipt.TxHash = tx.Hash() + receipt.GasUsed = msgResult.UsedGas + // if the transaction created a contract, store the creation address in the receipt. + if msg.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(evm.Context.Origin, tx.Nonce()) + } + // Set the receipt logs and create a bloom for filtering + receipt.Logs = statedb.GetLogs(tx.Hash()) + receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) + // These three are non-consensus fields + //receipt.BlockHash + //receipt.BlockNumber = + receipt.TransactionIndex = uint(txIndex) + receipts = append(receipts, receipt) + } + txIndex++ + } + statedb.IntermediateRoot(chainConfig.IsEIP158(vmContext.BlockNumber)) + // Add mining reward? + if miningReward > 0 { + // Add mining reward. The mining reward may be `0`, which only makes a difference in the cases + // where + // - the coinbase suicided, or + // - there are only 'bad' transactions, which aren't executed. In those cases, + // the coinbase gets no txfee, so isn't created, and thus needs to be touched + var ( + blockReward = big.NewInt(miningReward) + minerReward = new(big.Int).Set(blockReward) + perOmmer = new(big.Int).Div(blockReward, big.NewInt(32)) + ) + for _, ommer := range pre.Env.Ommers { + // Add 1/32th for each ommer included + minerReward.Add(minerReward, perOmmer) + // Add (8-delta)/8 + reward := big.NewInt(8) + reward.Sub(reward, big.NewInt(0).SetUint64(ommer.Delta)) + reward.Mul(reward, blockReward) + reward.Div(reward, big.NewInt(8)) + statedb.AddBalance(ommer.Address, reward) + } + statedb.AddBalance(pre.Env.Coinbase, minerReward) + } + // Commit block + root, err := statedb.Commit(chainConfig.IsEIP158(vmContext.BlockNumber)) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not commit state: %v", err) + return nil, nil, NewError(ErrorEVM, fmt.Errorf("could not commit state: %v", err)) + } + execRs := &ExecutionResult{ + StateRoot: root, + TxRoot: types.DeriveSha(includedTxs), + ReceiptRoot: types.DeriveSha(receipts), + Bloom: types.CreateBloom(receipts), + LogsHash: rlpHash(statedb.Logs()), + Receipts: receipts, + Rejected: rejectedTxs, + } + return statedb, execRs, nil +} + +func MakePreState(db ethdb.Database, accounts core.GenesisAlloc) *state.StateDB { + sdb := state.NewDatabase(db) + statedb, _ := state.New(common.Hash{}, sdb, nil) + for addr, a := range accounts { + statedb.SetCode(addr, a.Code) + statedb.SetNonce(addr, a.Nonce) + statedb.SetBalance(addr, a.Balance) + for k, v := range a.Storage { + statedb.SetState(addr, k, v) + } + } + // Commit and re-open to start with a clean state. + root, _ := statedb.Commit(false) + statedb, _ = state.New(root, sdb, nil) + return statedb +} + +func rlpHash(x interface{}) (h common.Hash) { + hw := sha3.NewLegacyKeccak256() + rlp.Encode(hw, x) + hw.Sum(h[:0]) + return h +} diff --git a/cmd/evm/internal/t8ntool/flags.go b/cmd/evm/internal/t8ntool/flags.go new file mode 100644 index 000000000..2e57d7258 --- /dev/null +++ b/cmd/evm/internal/t8ntool/flags.go @@ -0,0 +1,99 @@ +// Copyright 2020 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 t8ntool + +import ( + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/tests" + "gopkg.in/urfave/cli.v1" +) + +var ( + TraceFlag = cli.BoolFlag{ + Name: "trace", + Usage: "Output full trace logs to files .jsonl", + } + TraceDisableMemoryFlag = cli.BoolFlag{ + Name: "trace.nomemory", + Usage: "Disable full memory dump in traces", + } + TraceDisableStackFlag = cli.BoolFlag{ + Name: "trace.nostack", + Usage: "Disable stack output in traces", + } + OutputAllocFlag = cli.StringFlag{ + Name: "output.alloc", + Usage: "Determines where to put the `alloc` of the post-state.\n" + + "\t`stdout` - into the stdout output\n" + + "\t`stderr` - into the stderr output\n" + + "\t - into the file ", + Value: "alloc.json", + } + OutputResultFlag = cli.StringFlag{ + Name: "output.result", + Usage: "Determines where to put the `result` (stateroot, txroot etc) of the post-state.\n" + + "\t`stdout` - into the stdout output\n" + + "\t`stderr` - into the stderr output\n" + + "\t - into the file ", + Value: "result.json", + } + InputAllocFlag = cli.StringFlag{ + Name: "input.alloc", + Usage: "`stdin` or file name of where to find the prestate alloc to use.", + Value: "alloc.json", + } + InputEnvFlag = cli.StringFlag{ + Name: "input.env", + Usage: "`stdin` or file name of where to find the prestate env to use.", + Value: "env.json", + } + InputTxsFlag = cli.StringFlag{ + Name: "input.txs", + Usage: "`stdin` or file name of where to find the transactions to apply.", + Value: "txs.json", + } + RewardFlag = cli.Int64Flag{ + Name: "state.reward", + Usage: "Mining reward. Set to -1 to disable", + Value: 0, + } + ChainIDFlag = cli.Int64Flag{ + Name: "state.chainid", + Usage: "ChainID to use", + Value: 1, + } + ForknameFlag = cli.StringFlag{ + Name: "state.fork", + Usage: fmt.Sprintf("Name of ruleset to use."+ + "\n\tAvailable forknames:"+ + "\n\t %v"+ + "\n\tAvailable extra eips:"+ + "\n\t %v"+ + "\n\tSyntax (+ExtraEip)", + strings.Join(tests.AvailableForks(), "\n\t "), + strings.Join(vm.ActivateableEips(), ", ")), + Value: "Istanbul", + } + VerbosityFlag = cli.IntFlag{ + Name: "verbosity", + Usage: "sets the verbosity level", + Value: 3, + } +) diff --git a/cmd/evm/internal/t8ntool/gen_stenv.go b/cmd/evm/internal/t8ntool/gen_stenv.go new file mode 100644 index 000000000..ab5951534 --- /dev/null +++ b/cmd/evm/internal/t8ntool/gen_stenv.go @@ -0,0 +1,80 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package t8ntool + +import ( + "encoding/json" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" +) + +var _ = (*stEnvMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (s stEnv) MarshalJSON() ([]byte, error) { + type stEnv struct { + Coinbase common.UnprefixedAddress `json:"currentCoinbase" gencodec:"required"` + Difficulty *math.HexOrDecimal256 `json:"currentDifficulty" gencodec:"required"` + GasLimit math.HexOrDecimal64 `json:"currentGasLimit" gencodec:"required"` + Number math.HexOrDecimal64 `json:"currentNumber" gencodec:"required"` + Timestamp math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"` + BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"` + Ommers []ommer `json:"ommers,omitempty"` + } + var enc stEnv + enc.Coinbase = common.UnprefixedAddress(s.Coinbase) + enc.Difficulty = (*math.HexOrDecimal256)(s.Difficulty) + enc.GasLimit = math.HexOrDecimal64(s.GasLimit) + enc.Number = math.HexOrDecimal64(s.Number) + enc.Timestamp = math.HexOrDecimal64(s.Timestamp) + enc.BlockHashes = s.BlockHashes + enc.Ommers = s.Ommers + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (s *stEnv) UnmarshalJSON(input []byte) error { + type stEnv struct { + Coinbase *common.UnprefixedAddress `json:"currentCoinbase" gencodec:"required"` + Difficulty *math.HexOrDecimal256 `json:"currentDifficulty" gencodec:"required"` + GasLimit *math.HexOrDecimal64 `json:"currentGasLimit" gencodec:"required"` + Number *math.HexOrDecimal64 `json:"currentNumber" gencodec:"required"` + Timestamp *math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"` + BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"` + Ommers []ommer `json:"ommers,omitempty"` + } + var dec stEnv + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Coinbase == nil { + return errors.New("missing required field 'currentCoinbase' for stEnv") + } + s.Coinbase = common.Address(*dec.Coinbase) + if dec.Difficulty == nil { + return errors.New("missing required field 'currentDifficulty' for stEnv") + } + s.Difficulty = (*big.Int)(dec.Difficulty) + if dec.GasLimit == nil { + return errors.New("missing required field 'currentGasLimit' for stEnv") + } + s.GasLimit = uint64(*dec.GasLimit) + if dec.Number == nil { + return errors.New("missing required field 'currentNumber' for stEnv") + } + s.Number = uint64(*dec.Number) + if dec.Timestamp == nil { + return errors.New("missing required field 'currentTimestamp' for stEnv") + } + s.Timestamp = uint64(*dec.Timestamp) + if dec.BlockHashes != nil { + s.BlockHashes = dec.BlockHashes + } + if dec.Ommers != nil { + s.Ommers = dec.Ommers + } + return nil +} diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go new file mode 100644 index 000000000..a4908d763 --- /dev/null +++ b/cmd/evm/internal/t8ntool/transition.go @@ -0,0 +1,276 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package t8ntool + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math/big" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/tests" + "gopkg.in/urfave/cli.v1" +) + +const ( + ErrorEVM = 2 + ErrorVMConfig = 3 + ErrorMissingBlockhash = 4 + + ErrorJson = 10 + ErrorIO = 11 + + stdinSelector = "stdin" +) + +type NumberedError struct { + errorCode int + err error +} + +func NewError(errorCode int, err error) *NumberedError { + return &NumberedError{errorCode, err} +} + +func (n *NumberedError) Error() string { + return fmt.Sprintf("ERROR(%d): %v", n.errorCode, n.err.Error()) +} + +func (n *NumberedError) Code() int { + return n.errorCode +} + +type input struct { + Alloc core.GenesisAlloc `json:"alloc,omitempty"` + Env *stEnv `json:"env,omitempty"` + Txs types.Transactions `json:"txs,omitempty"` +} + +func Main(ctx *cli.Context) error { + // Configure the go-ethereum logger + glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false))) + glogger.Verbosity(log.Lvl(ctx.Int(VerbosityFlag.Name))) + log.Root().SetHandler(glogger) + + var ( + err error + tracer vm.Tracer + ) + var getTracer func(txIndex int) (vm.Tracer, error) + + if ctx.Bool(TraceFlag.Name) { + // Configure the EVM logger + logConfig := &vm.LogConfig{ + DisableStack: ctx.Bool(TraceDisableStackFlag.Name), + DisableMemory: ctx.Bool(TraceDisableMemoryFlag.Name), + Debug: true, + } + var prevFile *os.File + // This one closes the last file + defer func() { + if prevFile != nil { + prevFile.Close() + } + }() + getTracer = func(txIndex int) (vm.Tracer, error) { + if prevFile != nil { + prevFile.Close() + } + traceFile, err := os.Create(fmt.Sprintf("trace-%d.jsonl", txIndex)) + if err != nil { + return nil, NewError(ErrorIO, fmt.Errorf("failed creating trace-file: %v", err)) + } + prevFile = traceFile + return vm.NewJSONLogger(logConfig, traceFile), nil + } + } else { + getTracer = func(txIndex int) (tracer vm.Tracer, err error) { + return nil, nil + } + } + // We need to load three things: alloc, env and transactions. May be either in + // stdin input or in files. + // Check if anything needs to be read from stdin + var ( + prestate Prestate + txs types.Transactions // txs to apply + allocStr = ctx.String(InputAllocFlag.Name) + + envStr = ctx.String(InputEnvFlag.Name) + txStr = ctx.String(InputTxsFlag.Name) + inputData = &input{} + ) + + if allocStr == stdinSelector || envStr == stdinSelector || txStr == stdinSelector { + decoder := json.NewDecoder(os.Stdin) + decoder.Decode(inputData) + } + if allocStr != stdinSelector { + inFile, err := os.Open(allocStr) + if err != nil { + return NewError(ErrorIO, fmt.Errorf("failed reading alloc file: %v", err)) + } + defer inFile.Close() + decoder := json.NewDecoder(inFile) + if err := decoder.Decode(&inputData.Alloc); err != nil { + return NewError(ErrorJson, fmt.Errorf("Failed unmarshaling alloc-file: %v", err)) + } + } + + if envStr != stdinSelector { + inFile, err := os.Open(envStr) + if err != nil { + return NewError(ErrorIO, fmt.Errorf("failed reading env file: %v", err)) + } + defer inFile.Close() + decoder := json.NewDecoder(inFile) + var env stEnv + if err := decoder.Decode(&env); err != nil { + return NewError(ErrorJson, fmt.Errorf("Failed unmarshaling env-file: %v", err)) + } + inputData.Env = &env + } + + if txStr != stdinSelector { + inFile, err := os.Open(txStr) + if err != nil { + return NewError(ErrorIO, fmt.Errorf("failed reading txs file: %v", err)) + } + defer inFile.Close() + decoder := json.NewDecoder(inFile) + var txs types.Transactions + if err := decoder.Decode(&txs); err != nil { + return NewError(ErrorJson, fmt.Errorf("Failed unmarshaling txs-file: %v", err)) + } + inputData.Txs = txs + } + + prestate.Pre = inputData.Alloc + prestate.Env = *inputData.Env + txs = inputData.Txs + + // Iterate over all the tests, run them and aggregate the results + vmConfig := vm.Config{ + Tracer: tracer, + Debug: (tracer != nil), + } + // Construct the chainconfig + var chainConfig *params.ChainConfig + if cConf, extraEips, err := tests.GetChainConfig(ctx.String(ForknameFlag.Name)); err != nil { + return NewError(ErrorVMConfig, fmt.Errorf("Failed constructing chain configuration: %v", err)) + } else { + chainConfig = cConf + vmConfig.ExtraEips = extraEips + } + // Set the chain id + chainConfig.ChainID = big.NewInt(ctx.Int64(ChainIDFlag.Name)) + + // Run the test and aggregate the result + state, result, err := prestate.Apply(vmConfig, chainConfig, txs, ctx.Int64(RewardFlag.Name), getTracer) + if err != nil { + return err + } + // Dump the excution result + //postAlloc := state.DumpGenesisFormat(false, false, false) + collector := make(Alloc) + state.DumpToCollector(collector, false, false, false, nil, -1) + return dispatchOutput(ctx, result, collector) + +} + +type Alloc map[common.Address]core.GenesisAccount + +func (g Alloc) OnRoot(common.Hash) {} + +func (g Alloc) OnAccount(addr common.Address, dumpAccount state.DumpAccount) { + balance, _ := new(big.Int).SetString(dumpAccount.Balance, 10) + var storage map[common.Hash]common.Hash + if dumpAccount.Storage != nil { + storage = make(map[common.Hash]common.Hash) + for k, v := range dumpAccount.Storage { + storage[k] = common.HexToHash(v) + } + } + genesisAccount := core.GenesisAccount{ + Code: common.FromHex(dumpAccount.Code), + Storage: storage, + Balance: balance, + Nonce: dumpAccount.Nonce, + } + g[addr] = genesisAccount +} + +// saveFile marshalls the object to the given file +func saveFile(filename string, data interface{}) error { + b, err := json.MarshalIndent(data, "", " ") + if err != nil { + return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err)) + } + if err = ioutil.WriteFile(filename, b, 0644); err != nil { + return NewError(ErrorIO, fmt.Errorf("failed writing output: %v", err)) + } + return nil +} + +// dispatchOutput writes the output data to either stderr or stdout, or to the specified +// files +func dispatchOutput(ctx *cli.Context, result *ExecutionResult, alloc Alloc) error { + stdOutObject := make(map[string]interface{}) + stdErrObject := make(map[string]interface{}) + dispatch := func(fName, name string, obj interface{}) error { + switch fName { + case "stdout": + stdOutObject[name] = obj + case "stderr": + stdErrObject[name] = obj + default: // save to file + if err := saveFile(fName, obj); err != nil { + return err + } + } + return nil + } + if err := dispatch(ctx.String(OutputAllocFlag.Name), "alloc", alloc); err != nil { + return err + } + if err := dispatch(ctx.String(OutputResultFlag.Name), "result", result); err != nil { + return err + } + if len(stdOutObject) > 0 { + b, err := json.MarshalIndent(stdOutObject, "", " ") + if err != nil { + return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err)) + } + os.Stdout.Write(b) + } + if len(stdErrObject) > 0 { + b, err := json.MarshalIndent(stdErrObject, "", " ") + if err != nil { + return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err)) + } + os.Stderr.Write(b) + } + return nil +} diff --git a/cmd/evm/main.go b/cmd/evm/main.go index 72cb1ab85..473020d5f 100644 --- a/cmd/evm/main.go +++ b/cmd/evm/main.go @@ -22,6 +22,7 @@ import ( "math/big" "os" + "github.com/ethereum/go-ethereum/cmd/evm/internal/t8ntool" "github.com/ethereum/go-ethereum/cmd/utils" "gopkg.in/urfave/cli.v1" ) @@ -126,6 +127,27 @@ var ( } ) +var stateTransitionCommand = cli.Command{ + Name: "transition", + Aliases: []string{"t8n"}, + Usage: "executes a full state transition", + Action: t8ntool.Main, + Flags: []cli.Flag{ + t8ntool.TraceFlag, + t8ntool.TraceDisableMemoryFlag, + t8ntool.TraceDisableStackFlag, + t8ntool.OutputAllocFlag, + t8ntool.OutputResultFlag, + t8ntool.InputAllocFlag, + t8ntool.InputEnvFlag, + t8ntool.InputTxsFlag, + t8ntool.ForknameFlag, + t8ntool.ChainIDFlag, + t8ntool.RewardFlag, + t8ntool.VerbosityFlag, + }, +} + func init() { app.Flags = []cli.Flag{ BenchFlag, @@ -156,13 +178,18 @@ func init() { disasmCommand, runCommand, stateTestCommand, + stateTransitionCommand, } cli.CommandHelpTemplate = utils.OriginCommandHelpTemplate } func main() { if err := app.Run(os.Args); err != nil { + code := 1 + if ec, ok := err.(*t8ntool.NumberedError); ok { + code = ec.Code() + } fmt.Fprintln(os.Stderr, err) - os.Exit(1) + os.Exit(code) } } diff --git a/cmd/evm/poststate.json b/cmd/evm/poststate.json new file mode 100644 index 000000000..9ee17f18d --- /dev/null +++ b/cmd/evm/poststate.json @@ -0,0 +1,23 @@ +{ + "root": "f4157bb27bcb1d1a63001434a249a80948f2e9fe1f53d551244c1dae826b5b23", + "accounts": { + "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192": { + "balance": "4276951709", + "nonce": 1, + "root": "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "codeHash": "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "6916764286133345652", + "nonce": 172, + "root": "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "codeHash": "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + }, + "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "42500", + "nonce": 0, + "root": "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "codeHash": "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + } + } +} \ No newline at end of file diff --git a/cmd/evm/testdata/1/alloc.json b/cmd/evm/testdata/1/alloc.json new file mode 100644 index 000000000..cef1a25ff --- /dev/null +++ b/cmd/evm/testdata/1/alloc.json @@ -0,0 +1,12 @@ +{ + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x5ffd4878be161d74", + "code": "0x", + "nonce": "0xac", + "storage": {} + }, + "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192":{ + "balance": "0xfeedbead", + "nonce" : "0x00" + } +} \ No newline at end of file diff --git a/cmd/evm/testdata/1/env.json b/cmd/evm/testdata/1/env.json new file mode 100644 index 000000000..dd60abd20 --- /dev/null +++ b/cmd/evm/testdata/1/env.json @@ -0,0 +1,7 @@ +{ + "currentCoinbase": "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "currentDifficulty": "0x20000", + "currentGasLimit": "0x750a163df65e8a", + "currentNumber": "1", + "currentTimestamp": "1000" +} \ No newline at end of file diff --git a/cmd/evm/testdata/1/txs.json b/cmd/evm/testdata/1/txs.json new file mode 100644 index 000000000..50b31ff31 --- /dev/null +++ b/cmd/evm/testdata/1/txs.json @@ -0,0 +1,26 @@ +[ + { + "gas": "0x5208", + "gasPrice": "0x2", + "hash": "0x0557bacce3375c98d806609b8d5043072f0b6a8bae45ae5a67a00d3a1a18d673", + "input": "0x", + "nonce": "0x0", + "r": "0x9500e8ba27d3c33ca7764e107410f44cbd8c19794bde214d694683a7aa998cdb", + "s": "0x7235ae07e4bd6e0206d102b1f8979d6adab280466b6a82d2208ee08951f1f600", + "to": "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192", + "v": "0x1b", + "value": "0x1" + }, + { + "gas": "0x5208", + "gasPrice": "0x2", + "hash": "0x0557bacce3375c98d806609b8d5043072f0b6a8bae45ae5a67a00d3a1a18d673", + "input": "0x", + "nonce": "0x0", + "r": "0x9500e8ba27d3c33ca7764e107410f44cbd8c19794bde214d694683a7aa998cdb", + "s": "0x7235ae07e4bd6e0206d102b1f8979d6adab280466b6a82d2208ee08951f1f600", + "to": "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192", + "v": "0x1b", + "value": "0x1" + } +] diff --git a/cmd/evm/testdata/2/alloc.json b/cmd/evm/testdata/2/alloc.json new file mode 100644 index 000000000..a9720afc9 --- /dev/null +++ b/cmd/evm/testdata/2/alloc.json @@ -0,0 +1,16 @@ +{ + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x6001600053600160006001f0ff00", + "nonce" : "0x00", + "storage" : { + } + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x", + "nonce" : "0x00", + "storage" : { + } + } +} \ No newline at end of file diff --git a/cmd/evm/testdata/2/env.json b/cmd/evm/testdata/2/env.json new file mode 100644 index 000000000..ebadd3f06 --- /dev/null +++ b/cmd/evm/testdata/2/env.json @@ -0,0 +1,7 @@ +{ + "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty" : "0x020000", + "currentGasLimit" : "0x3b9aca00", + "currentNumber" : "0x01", + "currentTimestamp" : "0x03e8" +} \ No newline at end of file diff --git a/cmd/evm/testdata/2/readme.md b/cmd/evm/testdata/2/readme.md new file mode 100644 index 000000000..c116f0e79 --- /dev/null +++ b/cmd/evm/testdata/2/readme.md @@ -0,0 +1 @@ +These files examplify a selfdestruct to the `0`-address. \ No newline at end of file diff --git a/cmd/evm/testdata/2/txs.json b/cmd/evm/testdata/2/txs.json new file mode 100644 index 000000000..304445858 --- /dev/null +++ b/cmd/evm/testdata/2/txs.json @@ -0,0 +1,14 @@ +[ + { + "input" : "0x", + "gas" : "0x5f5e100", + "gasPrice" : "0x1", + "nonce" : "0x0", + "to" : "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + "value" : "0x186a0", + "v" : "0x1b", + "r" : "0x88544c93a564b4c28d2ffac2074a0c55fdd4658fe0d215596ed2e32e3ef7f56b", + "s" : "0x7fb4075d54190f825d7c47bb820284757b34fd6293904a93cddb1d3aa961ac28", + "hash" : "0x72fadbef39cd251a437eea619cfeda752271a5faaaa2147df012e112159ffb81" + } +] \ No newline at end of file diff --git a/cmd/evm/testdata/3/alloc.json b/cmd/evm/testdata/3/alloc.json new file mode 100644 index 000000000..dca318ee5 --- /dev/null +++ b/cmd/evm/testdata/3/alloc.json @@ -0,0 +1,16 @@ +{ + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x600140", + "nonce" : "0x00", + "storage" : { + } + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x", + "nonce" : "0x00", + "storage" : { + } + } +} \ No newline at end of file diff --git a/cmd/evm/testdata/3/env.json b/cmd/evm/testdata/3/env.json new file mode 100644 index 000000000..e283eff46 --- /dev/null +++ b/cmd/evm/testdata/3/env.json @@ -0,0 +1,8 @@ +{ + "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty" : "0x020000", + "currentGasLimit" : "0x3b9aca00", + "currentNumber" : "0x05", + "currentTimestamp" : "0x03e8", + "blockHashes" : { "1" : "0xdac58aa524e50956d0c0bae7f3f8bb9d35381365d07804dd5b48a5a297c06af4"} +} \ No newline at end of file diff --git a/cmd/evm/testdata/3/readme.md b/cmd/evm/testdata/3/readme.md new file mode 100644 index 000000000..499f03d7a --- /dev/null +++ b/cmd/evm/testdata/3/readme.md @@ -0,0 +1,2 @@ +These files examplify a transition where a transaction (excuted on block 5) requests +the blockhash for block `1`. diff --git a/cmd/evm/testdata/3/txs.json b/cmd/evm/testdata/3/txs.json new file mode 100644 index 000000000..304445858 --- /dev/null +++ b/cmd/evm/testdata/3/txs.json @@ -0,0 +1,14 @@ +[ + { + "input" : "0x", + "gas" : "0x5f5e100", + "gasPrice" : "0x1", + "nonce" : "0x0", + "to" : "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + "value" : "0x186a0", + "v" : "0x1b", + "r" : "0x88544c93a564b4c28d2ffac2074a0c55fdd4658fe0d215596ed2e32e3ef7f56b", + "s" : "0x7fb4075d54190f825d7c47bb820284757b34fd6293904a93cddb1d3aa961ac28", + "hash" : "0x72fadbef39cd251a437eea619cfeda752271a5faaaa2147df012e112159ffb81" + } +] \ No newline at end of file diff --git a/cmd/evm/testdata/4/alloc.json b/cmd/evm/testdata/4/alloc.json new file mode 100644 index 000000000..fadf2bdc4 --- /dev/null +++ b/cmd/evm/testdata/4/alloc.json @@ -0,0 +1,16 @@ +{ + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x600340", + "nonce" : "0x00", + "storage" : { + } + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x", + "nonce" : "0x00", + "storage" : { + } + } +} \ No newline at end of file diff --git a/cmd/evm/testdata/4/env.json b/cmd/evm/testdata/4/env.json new file mode 100644 index 000000000..e283eff46 --- /dev/null +++ b/cmd/evm/testdata/4/env.json @@ -0,0 +1,8 @@ +{ + "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty" : "0x020000", + "currentGasLimit" : "0x3b9aca00", + "currentNumber" : "0x05", + "currentTimestamp" : "0x03e8", + "blockHashes" : { "1" : "0xdac58aa524e50956d0c0bae7f3f8bb9d35381365d07804dd5b48a5a297c06af4"} +} \ No newline at end of file diff --git a/cmd/evm/testdata/4/readme.md b/cmd/evm/testdata/4/readme.md new file mode 100644 index 000000000..08840d37b --- /dev/null +++ b/cmd/evm/testdata/4/readme.md @@ -0,0 +1,3 @@ +These files examplify a transition where a transaction (excuted on block 5) requests +the blockhash for block `4`, but where the hash for that block is missing. +It's expected that executing these should cause `exit` with errorcode `4`. diff --git a/cmd/evm/testdata/4/txs.json b/cmd/evm/testdata/4/txs.json new file mode 100644 index 000000000..304445858 --- /dev/null +++ b/cmd/evm/testdata/4/txs.json @@ -0,0 +1,14 @@ +[ + { + "input" : "0x", + "gas" : "0x5f5e100", + "gasPrice" : "0x1", + "nonce" : "0x0", + "to" : "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + "value" : "0x186a0", + "v" : "0x1b", + "r" : "0x88544c93a564b4c28d2ffac2074a0c55fdd4658fe0d215596ed2e32e3ef7f56b", + "s" : "0x7fb4075d54190f825d7c47bb820284757b34fd6293904a93cddb1d3aa961ac28", + "hash" : "0x72fadbef39cd251a437eea619cfeda752271a5faaaa2147df012e112159ffb81" + } +] \ No newline at end of file diff --git a/cmd/evm/testdata/5/alloc.json b/cmd/evm/testdata/5/alloc.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/cmd/evm/testdata/5/alloc.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/cmd/evm/testdata/5/env.json b/cmd/evm/testdata/5/env.json new file mode 100644 index 000000000..1085f63e6 --- /dev/null +++ b/cmd/evm/testdata/5/env.json @@ -0,0 +1,11 @@ +{ + "currentCoinbase": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "currentDifficulty": "0x20000", + "currentGasLimit": "0x750a163df65e8a", + "currentNumber": "1", + "currentTimestamp": "1000", + "ommers": [ + {"delta": 1, "address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }, + {"delta": 2, "address": "0xcccccccccccccccccccccccccccccccccccccccc" } + ] +} \ No newline at end of file diff --git a/cmd/evm/testdata/5/readme.md b/cmd/evm/testdata/5/readme.md new file mode 100644 index 000000000..e2b608fac --- /dev/null +++ b/cmd/evm/testdata/5/readme.md @@ -0,0 +1 @@ +These files examplify a transition where there are no transcations, two ommers, at block `N-1` (delta 1) and `N-2` (delta 2). \ No newline at end of file diff --git a/cmd/evm/testdata/5/txs.json b/cmd/evm/testdata/5/txs.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/cmd/evm/testdata/5/txs.json @@ -0,0 +1 @@ +[] diff --git a/cmd/evm/testdata/7/alloc.json b/cmd/evm/testdata/7/alloc.json new file mode 100644 index 000000000..cef1a25ff --- /dev/null +++ b/cmd/evm/testdata/7/alloc.json @@ -0,0 +1,12 @@ +{ + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x5ffd4878be161d74", + "code": "0x", + "nonce": "0xac", + "storage": {} + }, + "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192":{ + "balance": "0xfeedbead", + "nonce" : "0x00" + } +} \ No newline at end of file diff --git a/cmd/evm/testdata/7/env.json b/cmd/evm/testdata/7/env.json new file mode 100644 index 000000000..8fd9bc041 --- /dev/null +++ b/cmd/evm/testdata/7/env.json @@ -0,0 +1,7 @@ +{ + "currentCoinbase": "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "currentDifficulty": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff020000", + "currentGasLimit": "0x750a163df65e8a", + "currentNumber": "5", + "currentTimestamp": "1000" +} \ No newline at end of file diff --git a/cmd/evm/testdata/7/readme.md b/cmd/evm/testdata/7/readme.md new file mode 100644 index 000000000..c9826e0ba --- /dev/null +++ b/cmd/evm/testdata/7/readme.md @@ -0,0 +1,7 @@ +This is a test for HomesteadToDao, checking if the +DAO-transition works + +Example: +``` +./statet8n --input.alloc=./testdata/7/alloc.json --input.txs=./testdata/7/txs.json --input.env=./testdata/7/env.json --output.alloc=stdout --state.fork=HomesteadToDaoAt5 +``` \ No newline at end of file diff --git a/cmd/evm/testdata/7/txs.json b/cmd/evm/testdata/7/txs.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/cmd/evm/testdata/7/txs.json @@ -0,0 +1 @@ +[] diff --git a/cmd/evm/transition-test.sh b/cmd/evm/transition-test.sh new file mode 100644 index 000000000..d1400ca57 --- /dev/null +++ b/cmd/evm/transition-test.sh @@ -0,0 +1,191 @@ +#!/bin/bash +ticks="\`\`\`" + +function showjson(){ + echo "\`$1\`:" + echo "${ticks}json" + cat $1 + echo "" + echo "$ticks" +} +function demo(){ + echo "$ticks" + echo "$1" + echo "$ticks" + echo "" +} +function tick(){ + echo "$ticks" +} + +cat << EOF +## EVM state transition tool + +The \`evm t8n\` tool is a stateless state transition utility. It is a utility +which can + +1. Take a prestate, including + - Accounts, + - Block context information, + - Previous blockshashes (*optional) +2. Apply a set of transactions, +3. Apply a mining-reward (*optional), +4. And generate a post-state, including + - State root, transaction root, receipt root, + - Information about rejected transactions, + - Optionally: a full or partial post-state dump + +## Specification + +The idea is to specify the behaviour of this binary very _strict_, so that other +node implementors can build replicas based on their own state-machines, and the +state generators can swap between a \`geth\`-based implementation and a \`parityvm\`-based +implementation. + +### Command line params + +Command line params that has to be supported are +$(tick) + +` ./evm t8n -h | grep "trace\|output\|state\."` + +$(tick) + +### Error codes and output + +All logging should happen against the \`stderr\`. +There are a few (not many) errors that can occur, those are defined below. + +#### EVM-based errors (\`2\` to \`9\`) + +- Other EVM error. Exit code \`2\` +- Failed configuration: when a non-supported or invalid fork was specified. Exit code \`3\`. +- Block history is not supplied, but needed for a \`BLOCKHASH\` operation. If \`BLOCKHASH\` + is invoked targeting a block which history has not been provided for, the program will + exit with code \`4\`. + +#### IO errors (\`10\`-\`20\`) + +- Invalid input json: the supplied data could not be marshalled. + The program will exit with code \`10\` +- IO problems: failure to load or save files, the program will exit with code \`11\` + +EOF + +# This should exit with 3 +./evm t8n --input.alloc=./testdata/1/alloc.json --input.txs=./testdata/1/txs.json --input.env=./testdata/1/env.json --state.fork=Frontier+1346 2>/dev/null +if [ $? != 3 ]; then + echo "Failed, exitcode should be 3" +fi +cat << EOF +## Examples +### Basic usage + +Invoking it with the provided example files +EOF +cmd="./evm t8n --input.alloc=./testdata/1/alloc.json --input.txs=./testdata/1/txs.json --input.env=./testdata/1/env.json" +tick;echo "$cmd"; tick +$cmd 2>/dev/null +echo "Two resulting files:" +echo "" +showjson alloc.json +showjson result.json +echo "" + +echo "We can make them spit out the data to e.g. \`stdout\` like this:" +cmd="./evm t8n --input.alloc=./testdata/1/alloc.json --input.txs=./testdata/1/txs.json --input.env=./testdata/1/env.json --output.result=stdout --output.alloc=stdout" +tick;echo "$cmd"; tick +output=`$cmd 2>/dev/null` +echo "Output:" +echo "${ticks}json" +echo "$output" +echo "$ticks" + +cat << EOF + +## About Ommers + +Mining rewards and ommer rewards might need to be added. This is how those are applied: + +- \`block_reward\` is the block mining reward for the miner (\`0xaa\`), of a block at height \`N\`. +- For each ommer (mined by \`0xbb\`), with blocknumber \`N-delta\` + - (where \`delta\` is the difference between the current block and the ommer) + - The account \`0xbb\` (ommer miner) is awarded \`(8-delta)/ 8 * block_reward\` + - The account \`0xaa\` (block miner) is awarded \`block_reward / 32\` + +To make \`state_t8n\` apply these, the following inputs are required: + +- \`state.reward\` + - For ethash, it is \`5000000000000000000\` \`wei\`, + - If this is not defined, mining rewards are not applied, + - A value of \`0\` is valid, and causes accounts to be 'touched'. +- For each ommer, the tool needs to be given an \`address\` and a \`delta\`. This + is done via the \`env\`. + +Note: the tool does not verify that e.g. the normal uncle rules apply, +and allows e.g two uncles at the same height, or the uncle-distance. This means that +the tool allows for negative uncle reward (distance > 8) + +Example: +EOF + +showjson ./testdata/5/env.json + +echo "When applying this, using a reward of \`0x08\`" +cmd="./evm t8n --input.alloc=./testdata/5/alloc.json -input.txs=./testdata/5/txs.json --input.env=./testdata/5/env.json --output.alloc=stdout --state.reward=0x80" +output=`$cmd 2>/dev/null` +echo "Output:" +echo "${ticks}json" +echo "$output" +echo "$ticks" + +echo "### Future EIPS" +echo "" +echo "It is also possible to experiment with future eips that are not yet defined in a hard fork." +echo "Example, putting EIP-1344 into Frontier: " +cmd="./evm t8n --state.fork=Frontier+1344 --input.pre=./testdata/1/pre.json --input.txs=./testdata/1/txs.json --input.env=/testdata/1/env.json" +tick;echo "$cmd"; tick +echo "" + +echo "### Block history" +echo "" +echo "The \`BLOCKHASH\` opcode requires blockhashes to be provided by the caller, inside the \`env\`." +echo "If a required blockhash is not provided, the exit code should be \`4\`:" +echo "Example where blockhashes are provided: " +cmd="./evm t8n --input.alloc=./testdata/3/alloc.json --input.txs=./testdata/3/txs.json --input.env=./testdata/3/env.json --trace" +tick && echo $cmd && tick +$cmd 2>&1 >/dev/null +cmd="cat trace-0.jsonl | grep BLOCKHASH -C2" +tick && echo $cmd && tick +echo "$ticks" +cat trace-0.jsonl | grep BLOCKHASH -C2 +echo "$ticks" +echo "" + +echo "In this example, the caller has not provided the required blockhash:" +cmd="./evm t8n --input.alloc=./testdata/4/alloc.json --input.txs=./testdata/4/txs.json --input.env=./testdata/4/env.json --trace" +tick && echo $cmd && tick +tick +$cmd +errc=$? +tick +echo "Error code: $errc" + + +echo "### Chaining" +echo "" +echo "Another thing that can be done, is to chain invocations:" +cmd1="./evm t8n --input.alloc=./testdata/1/alloc.json --input.txs=./testdata/1/txs.json --input.env=./testdata/1/env.json --output.alloc=stdout" +cmd2="./evm t8n --input.alloc=stdin --input.env=./testdata/1/env.json --input.txs=./testdata/1/txs.json" +echo "$ticks" +echo "$cmd1 | $cmd2" +output=$($cmd1 | $cmd2 ) +echo $output +echo "$ticks" +echo "What happened here, is that we first applied two identical transactions, so the second one was rejected. " +echo "Then, taking the poststate alloc as the input for the next state, we tried again to include" +echo "the same two transactions: this time, both failed due to too low nonce." +echo "" +echo "In order to meaningfully chain invocations, one would need to provide meaningful new \`env\`, otherwise the" +echo "actual blocknumber (exposed to the EVM) would not increase." +echo "" diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 088f012fe..445f476cf 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -71,8 +71,8 @@ var ( {{if .cmd.Description}}{{.cmd.Description}} {{end}}{{if .cmd.Subcommands}} SUBCOMMANDS: - {{range .cmd.Subcommands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} - {{end}}{{end}}{{if .categorizedFlags}} + {{range .cmd.Subcommands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{end}}{{end}}{{if .categorizedFlags}} {{range $idx, $categorized := .categorizedFlags}}{{$categorized.Name}} OPTIONS: {{range $categorized.Flags}}{{"\t"}}{{.}} {{end}} @@ -82,10 +82,10 @@ SUBCOMMANDS: {{if .Description}}{{.Description}} {{end}}{{if .Subcommands}} SUBCOMMANDS: - {{range .Subcommands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} - {{end}}{{end}}{{if .Flags}} + {{range .Subcommands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{end}}{{end}}{{if .Flags}} OPTIONS: -{{range $.Flags}}{{"\t"}}{{.}} +{{range $.Flags}} {{.}} {{end}} {{end}}` ) diff --git a/core/state/dump.go b/core/state/dump.go index 8224e160b..9bb946d14 100644 --- a/core/state/dump.go +++ b/core/state/dump.go @@ -27,6 +27,14 @@ import ( "github.com/ethereum/go-ethereum/trie" ) +// DumpCollector interface which the state trie calls during iteration +type DumpCollector interface { + // OnRoot is called with the state root + OnRoot(common.Hash) + // OnAccount is called once for each account in the trie + OnAccount(common.Address, DumpAccount) +} + // DumpAccount represents an account in the state. type DumpAccount struct { Balance string `json:"balance"` @@ -46,9 +54,14 @@ type Dump struct { Accounts map[common.Address]DumpAccount `json:"accounts"` } -// iterativeDump is a 'collector'-implementation which dump output line-by-line iteratively. -type iterativeDump struct { - *json.Encoder +// OnRoot implements DumpCollector interface +func (d *Dump) OnRoot(root common.Hash) { + d.Root = fmt.Sprintf("%x", root) +} + +// OnAccount implements DumpCollector interface +func (d *Dump) OnAccount(addr common.Address, account DumpAccount) { + d.Accounts[addr] = account } // IteratorDump is an implementation for iterating over data. @@ -58,28 +71,23 @@ type IteratorDump struct { Next []byte `json:"next,omitempty"` // nil if no more accounts } -// Collector interface which the state trie calls during iteration -type collector interface { - onRoot(common.Hash) - onAccount(common.Address, DumpAccount) -} - -func (d *Dump) onRoot(root common.Hash) { +// OnRoot implements DumpCollector interface +func (d *IteratorDump) OnRoot(root common.Hash) { d.Root = fmt.Sprintf("%x", root) } -func (d *Dump) onAccount(addr common.Address, account DumpAccount) { - d.Accounts[addr] = account -} -func (d *IteratorDump) onRoot(root common.Hash) { - d.Root = fmt.Sprintf("%x", root) -} - -func (d *IteratorDump) onAccount(addr common.Address, account DumpAccount) { +// OnAccount implements DumpCollector interface +func (d *IteratorDump) OnAccount(addr common.Address, account DumpAccount) { d.Accounts[addr] = account } -func (d iterativeDump) onAccount(addr common.Address, account DumpAccount) { +// iterativeDump is a DumpCollector-implementation which dumps output line-by-line iteratively. +type iterativeDump struct { + *json.Encoder +} + +// OnAccount implements DumpCollector interface +func (d iterativeDump) OnAccount(addr common.Address, account DumpAccount) { dumpAccount := &DumpAccount{ Balance: account.Balance, Nonce: account.Nonce, @@ -96,15 +104,16 @@ func (d iterativeDump) onAccount(addr common.Address, account DumpAccount) { d.Encode(dumpAccount) } -func (d iterativeDump) onRoot(root common.Hash) { +// OnRoot implements DumpCollector interface +func (d iterativeDump) OnRoot(root common.Hash) { d.Encode(struct { Root common.Hash `json:"root"` }{root}) } -func (s *StateDB) dump(c collector, excludeCode, excludeStorage, excludeMissingPreimages bool, start []byte, maxResults int) (nextKey []byte) { +func (s *StateDB) DumpToCollector(c DumpCollector, excludeCode, excludeStorage, excludeMissingPreimages bool, start []byte, maxResults int) (nextKey []byte) { missingPreimages := 0 - c.onRoot(s.trie.Hash()) + c.OnRoot(s.trie.Hash()) var count int it := trie.NewIterator(s.trie.NodeIterator(start)) @@ -145,7 +154,7 @@ func (s *StateDB) dump(c collector, excludeCode, excludeStorage, excludeMissingP account.Storage[common.BytesToHash(s.trie.GetKey(storageIt.Key))] = common.Bytes2Hex(content) } } - c.onAccount(addr, account) + c.OnAccount(addr, account) count++ if maxResults > 0 && count >= maxResults { if it.Next() { @@ -166,7 +175,7 @@ func (s *StateDB) RawDump(excludeCode, excludeStorage, excludeMissingPreimages b dump := &Dump{ Accounts: make(map[common.Address]DumpAccount), } - s.dump(dump, excludeCode, excludeStorage, excludeMissingPreimages, nil, 0) + s.DumpToCollector(dump, excludeCode, excludeStorage, excludeMissingPreimages, nil, 0) return *dump } @@ -175,14 +184,14 @@ func (s *StateDB) Dump(excludeCode, excludeStorage, excludeMissingPreimages bool dump := s.RawDump(excludeCode, excludeStorage, excludeMissingPreimages) json, err := json.MarshalIndent(dump, "", " ") if err != nil { - fmt.Println("dump err", err) + fmt.Println("Dump err", err) } return json } // IterativeDump dumps out accounts as json-objects, delimited by linebreaks on stdout func (s *StateDB) IterativeDump(excludeCode, excludeStorage, excludeMissingPreimages bool, output *json.Encoder) { - s.dump(iterativeDump{output}, excludeCode, excludeStorage, excludeMissingPreimages, nil, 0) + s.DumpToCollector(iterativeDump{output}, excludeCode, excludeStorage, excludeMissingPreimages, nil, 0) } // IteratorDump dumps out a batch of accounts starts with the given start key @@ -190,6 +199,6 @@ func (s *StateDB) IteratorDump(excludeCode, excludeStorage, excludeMissingPreima iterator := &IteratorDump{ Accounts: make(map[common.Address]DumpAccount), } - iterator.Next = s.dump(iterator, excludeCode, excludeStorage, excludeMissingPreimages, start, maxResults) + iterator.Next = s.DumpToCollector(iterator, excludeCode, excludeStorage, excludeMissingPreimages, start, maxResults) return *iterator } diff --git a/core/state/state_test.go b/core/state/state_test.go index 41d9b4655..0dc4c0ad6 100644 --- a/core/state/state_test.go +++ b/core/state/state_test.go @@ -56,7 +56,7 @@ func TestDump(t *testing.T) { s.state.updateStateObject(obj2) s.state.Commit(false) - // check that dump contains the state objects that are in trie + // check that DumpToCollector contains the state objects that are in trie got := string(s.state.Dump(false, false, true)) want := `{ "root": "71edff0130dd2385947095001c73d9e28d862fc286fca2b922ca6f6f3cddfdd2", @@ -83,7 +83,7 @@ func TestDump(t *testing.T) { } }` if got != want { - t.Errorf("dump mismatch:\ngot: %s\nwant: %s\n", got, want) + t.Errorf("DumpToCollector mismatch:\ngot: %s\nwant: %s\n", got, want) } } diff --git a/core/vm/eips.go b/core/vm/eips.go index 0f33659ea..142cfdd84 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -18,30 +18,44 @@ package vm import ( "fmt" + "sort" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) +var activators = map[int]func(*JumpTable){ + 2200: enable2200, + 1884: enable1884, + 1344: enable1344, + 2315: enable2315, +} + // EnableEIP enables the given EIP on the config. // This operation writes in-place, and callers need to ensure that the globally // defined jump tables are not polluted. func EnableEIP(eipNum int, jt *JumpTable) error { - switch eipNum { - case 2200: - enable2200(jt) - case 1884: - enable1884(jt) - case 1344: - enable1344(jt) - case 2315: - enable2315(jt) - default: + enablerFn, ok := activators[eipNum] + if !ok { return fmt.Errorf("undefined eip %d", eipNum) } + enablerFn(jt) return nil } +func ValidEip(eipNum int) bool { + _, ok := activators[eipNum] + return ok +} +func ActivateableEips() []string { + var nums []string + for k := range activators { + nums = append(nums, fmt.Sprintf("%d", k)) + } + sort.Strings(nums) + return nums +} + // enable1884 applies EIP-1884 to the given jump table: // - Increase cost of BALANCE to 700 // - Increase cost of EXTCODEHASH to 700 diff --git a/tests/init.go b/tests/init.go index 70382d356..6c30c3537 100644 --- a/tests/init.go +++ b/tests/init.go @@ -19,6 +19,7 @@ package tests import ( "fmt" "math/big" + "sort" "github.com/ethereum/go-ethereum/params" ) @@ -154,6 +155,16 @@ var Forks = map[string]*params.ChainConfig{ }, } +// Returns the set of defined fork names +func AvailableForks() []string { + var availableForks []string + for k := range Forks { + availableForks = append(availableForks, k) + } + sort.Strings(availableForks) + return availableForks +} + // UnsupportedForkError is returned when a test requests a fork that isn't implemented. type UnsupportedForkError struct { Name string diff --git a/tests/state_test_util.go b/tests/state_test_util.go index a8d6fac51..a999cba47 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -112,11 +112,11 @@ type stTransactionMarshaling struct { PrivateKey hexutil.Bytes } -// getVMConfig takes a fork definition and returns a chain config. +// GetChainConfig takes a fork definition and returns a chain config. // The fork definition can be // - a plain forkname, e.g. `Byzantium`, // - a fork basename, and a list of EIPs to enable; e.g. `Byzantium+1884+1283`. -func getVMConfig(forkString string) (baseConfig *params.ChainConfig, eips []int, err error) { +func GetChainConfig(forkString string) (baseConfig *params.ChainConfig, eips []int, err error) { var ( splitForks = strings.Split(forkString, "+") ok bool @@ -129,6 +129,9 @@ func getVMConfig(forkString string) (baseConfig *params.ChainConfig, eips []int, if eipNum, err := strconv.Atoi(eip); err != nil { return nil, nil, fmt.Errorf("syntax error, invalid eip number %v", eipNum) } else { + if !vm.ValidEip(eipNum) { + return nil, nil, fmt.Errorf("syntax error, invalid eip number %v", eipNum) + } eips = append(eips, eipNum) } } @@ -166,7 +169,7 @@ func (t *StateTest) Run(subtest StateSubtest, vmconfig vm.Config, snapshotter bo // RunNoVerify runs a specific subtest and returns the statedb and post-state root func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config, snapshotter bool) (*snapshot.Tree, *state.StateDB, common.Hash, error) { - config, eips, err := getVMConfig(subtest.Fork) + config, eips, err := GetChainConfig(subtest.Fork) if err != nil { return nil, nil, common.Hash{}, UnsupportedForkError{subtest.Fork} }