erigon-pulse/cmd/evm/internal/t8ntool/transition.go
ledgerwatch ee83447fa1
CL lightclient to create memdb in the tmpdir which gets cleaned up on… (#6829)
… startup

---------

Co-authored-by: Alexey Sharp <alexeysharp@Alexeys-iMac.local>
Co-authored-by: Alex Sharp <alexsharp@Alexs-MacBook-Pro-2.local>
2023-02-11 20:44:51 +00:00

632 lines
19 KiB
Go

// 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 <http://www.gnu.org/licenses/>.
package t8ntool
import (
"context"
"crypto/ecdsa"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"path"
"path/filepath"
"github.com/holiman/uint256"
"github.com/ledgerwatch/erigon-lib/chain"
libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon-lib/common/length"
"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/erigon-lib/kv/kvcfg"
"github.com/ledgerwatch/erigon-lib/kv/memdb"
"github.com/ledgerwatch/log/v3"
"github.com/urfave/cli/v2"
"github.com/ledgerwatch/erigon/cmd/rpcdaemon/commands"
"github.com/ledgerwatch/erigon/common"
"github.com/ledgerwatch/erigon/common/hexutil"
"github.com/ledgerwatch/erigon/common/math"
"github.com/ledgerwatch/erigon/consensus/ethash"
"github.com/ledgerwatch/erigon/core"
"github.com/ledgerwatch/erigon/core/state"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/core/vm"
"github.com/ledgerwatch/erigon/crypto"
"github.com/ledgerwatch/erigon/eth/tracers/logger"
"github.com/ledgerwatch/erigon/rlp"
"github.com/ledgerwatch/erigon/tests"
"github.com/ledgerwatch/erigon/turbo/trie"
)
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) ExitCode() int {
return n.errorCode
}
// compile-time conformance test
var (
_ cli.ExitCoder = (*NumberedError)(nil)
)
type input struct {
Alloc core.GenesisAlloc `json:"alloc,omitempty"`
Env *stEnv `json:"env,omitempty"`
Txs []*txWithKey `json:"txs,omitempty"`
}
func Main(ctx *cli.Context) error {
log.Root().SetHandler(log.LvlFilterHandler(log.LvlInfo, log.StderrHandler))
var (
err error
baseDir = ""
)
var getTracer func(txIndex int, txHash libcommon.Hash) (vm.EVMLogger, error)
// If user specified a basedir, make sure it exists
if ctx.IsSet(OutputBasedir.Name) {
if base := ctx.String(OutputBasedir.Name); len(base) > 0 {
err2 := os.MkdirAll(base, 0755) // //rw-r--r--
if err2 != nil {
return NewError(ErrorIO, fmt.Errorf("failed creating output basedir: %v", err2))
}
baseDir = base
}
}
if ctx.Bool(TraceFlag.Name) {
// Configure the EVM logger
logConfig := &logger.LogConfig{
DisableStack: ctx.Bool(TraceDisableStackFlag.Name),
DisableMemory: ctx.Bool(TraceDisableMemoryFlag.Name),
DisableReturnData: ctx.Bool(TraceDisableReturnDataFlag.Name),
Debug: true,
}
var prevFile *os.File
// This one closes the last file
defer func() {
if prevFile != nil {
prevFile.Close()
}
}()
getTracer = func(txIndex int, txHash libcommon.Hash) (vm.EVMLogger, error) {
if prevFile != nil {
prevFile.Close()
}
traceFile, err2 := os.Create(path.Join(baseDir, fmt.Sprintf("trace-%d-%v.jsonl", txIndex, txHash.String())))
if err2 != nil {
return nil, NewError(ErrorIO, fmt.Errorf("failed creating trace-file: %v", err2))
}
prevFile = traceFile
return logger.NewJSONLogger(logConfig, traceFile), nil
}
} else {
getTracer = func(txIndex int, txHash libcommon.Hash) (tracer vm.EVMLogger, 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{}
)
// Figure out the prestate alloc
if allocStr == stdinSelector || envStr == stdinSelector || txStr == stdinSelector {
decoder := json.NewDecoder(os.Stdin)
decoder.Decode(inputData) //nolint:errcheck
}
if allocStr != stdinSelector {
inFile, err1 := os.Open(allocStr)
if err1 != nil {
return NewError(ErrorIO, fmt.Errorf("failed reading alloc file: %v", err1))
}
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))
}
}
prestate.Pre = inputData.Alloc
// Set the block environment
if envStr != stdinSelector {
inFile, err1 := os.Open(envStr)
if err1 != nil {
return NewError(ErrorIO, fmt.Errorf("failed reading env file: %v", err1))
}
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
}
prestate.Env = *inputData.Env
vmConfig := vm.Config{
Tracer: nil,
Debug: ctx.Bool(TraceFlag.Name),
StatelessExec: true,
}
// Construct the chainconfig
var chainConfig *chain.Config
if cConf, extraEips, err1 := tests.GetChainConfig(ctx.String(ForknameFlag.Name)); err1 != nil {
return NewError(ErrorVMConfig, fmt.Errorf("failed constructing chain configuration: %v", err1))
} else { //nolint:golint
chainConfig = cConf
vmConfig.ExtraEips = extraEips
}
// Set the chain id
chainConfig.ChainID = big.NewInt(ctx.Int64(ChainIDFlag.Name))
var txsWithKeys []*txWithKey
if txStr != stdinSelector {
inFile, err1 := os.Open(txStr)
if err1 != nil {
return NewError(ErrorIO, fmt.Errorf("failed reading txs file: %v", err1))
}
defer inFile.Close()
decoder := json.NewDecoder(inFile)
if err = decoder.Decode(&txsWithKeys); err != nil {
return NewError(ErrorJson, fmt.Errorf("failed unmarshaling txs-file: %v", err))
}
} else {
txsWithKeys = inputData.Txs
}
// We may have to sign the transactions.
signer := types.MakeSigner(chainConfig, prestate.Env.Number)
if txs, err = signUnsignedTransactions(txsWithKeys, *signer); err != nil {
return NewError(ErrorJson, fmt.Errorf("failed signing transactions: %v", err))
}
eip1559 := chainConfig.IsLondon(prestate.Env.Number)
// Sanity check, to not `panic` in state_transition
if eip1559 {
if prestate.Env.BaseFee == nil {
return NewError(ErrorVMConfig, errors.New("EIP-1559 config but missing 'currentBaseFee' in env section"))
}
}
// Sanity check, to not `panic` in state_transition
if prestate.Env.Random != nil && !eip1559 {
return NewError(ErrorVMConfig, errors.New("can only apply RANDOM on top of London chain rules"))
}
if chainConfig.IsShanghai(prestate.Env.Timestamp) && prestate.Env.Withdrawals == nil {
return NewError(ErrorVMConfig, errors.New("Shanghai config but missing 'withdrawals' in env section"))
}
if env := prestate.Env; env.Difficulty == nil {
// If difficulty was not provided by caller, we need to calculate it.
switch {
case env.ParentDifficulty == nil:
return NewError(ErrorVMConfig, errors.New("currentDifficulty was not provided, and cannot be calculated due to missing parentDifficulty"))
case env.Number == 0:
return NewError(ErrorVMConfig, errors.New("currentDifficulty needs to be provided for block number 0"))
case env.Timestamp <= env.ParentTimestamp:
return NewError(ErrorVMConfig, fmt.Errorf("currentDifficulty cannot be calculated -- currentTime (%d) needs to be after parent time (%d)",
env.Timestamp, env.ParentTimestamp))
}
prestate.Env.Difficulty = calcDifficulty(chainConfig, env.Number, env.Timestamp,
env.ParentTimestamp, env.ParentDifficulty, env.ParentUncleHash)
}
// manufacture block from above inputs
header := NewHeader(prestate.Env)
var ommerHeaders = make([]*types.Header, len(prestate.Env.Ommers))
header.Number.Add(header.Number, big.NewInt(int64(len(prestate.Env.Ommers))))
for i, ommer := range prestate.Env.Ommers {
var ommerN big.Int
ommerN.SetUint64(header.Number.Uint64() - ommer.Delta)
ommerHeaders[i] = &types.Header{Coinbase: ommer.Address, Number: &ommerN}
}
block := types.NewBlock(header, txs, ommerHeaders, nil /* receipts */, prestate.Env.Withdrawals)
var hashError error
getHash := func(num uint64) libcommon.Hash {
if prestate.Env.BlockHashes == nil {
hashError = fmt.Errorf("getHash(%d) invoked, no blockhashes provided", num)
return libcommon.Hash{}
}
h, ok := prestate.Env.BlockHashes[math.HexOrDecimal64(num)]
if !ok {
hashError = fmt.Errorf("getHash(%d) invoked, blockhash for that block not provided", num)
}
return h
}
db := memdb.New("" /* tmpDir */)
tx, err := db.BeginRw(context.Background())
if err != nil {
return err
}
defer tx.Rollback()
reader, writer := MakePreState(chainConfig.Rules(0, 0), tx, prestate.Pre)
engine := ethash.NewFaker()
result, err := core.ExecuteBlockEphemerally(chainConfig, &vmConfig, getHash, engine, block, reader, writer, nil, nil, getTracer)
if hashError != nil {
return NewError(ErrorMissingBlockhash, fmt.Errorf("blockhash error: %v", err))
}
if err != nil {
return fmt.Errorf("error on EBE: %w", err)
}
// state root calculation
root, err := CalculateStateRoot(tx)
if err != nil {
return err
}
result.StateRoot = *root
// Dump the execution result
body, _ := rlp.EncodeToBytes(txs)
collector := make(Alloc)
historyV3, err := kvcfg.HistoryV3.Enabled(tx)
if err != nil {
return err
}
dumper := state.NewDumper(tx, prestate.Env.Number, historyV3)
dumper.DumpToCollector(collector, false, false, libcommon.Address{}, 0)
return dispatchOutput(ctx, baseDir, result, collector, body)
}
// txWithKey is a helper-struct, to allow us to use the types.Transaction along with
// a `secretKey`-field, for input
type txWithKey struct {
key *ecdsa.PrivateKey
tx types.Transaction
}
func (t *txWithKey) UnmarshalJSON(input []byte) error {
// Read the secretKey, if present
type sKey struct {
Key *libcommon.Hash `json:"secretKey"`
}
var key sKey
if err := json.Unmarshal(input, &key); err != nil {
return err
}
if key.Key != nil {
k := key.Key.Hex()[2:]
if ecdsaKey, err := crypto.HexToECDSA(k); err == nil {
t.key = ecdsaKey
} else {
return err
}
}
// Now, read the transaction itself
var txJson commands.RPCTransaction
if err := json.Unmarshal(input, &txJson); err != nil {
return err
}
// assemble transaction
tx, err := getTransaction(txJson)
if err != nil {
return err
}
t.tx = tx
return nil
}
func getTransaction(txJson commands.RPCTransaction) (types.Transaction, error) {
gasPrice, value := uint256.NewInt(0), uint256.NewInt(0)
var overflow bool
var chainId *uint256.Int
if txJson.Value != nil {
value, overflow = uint256.FromBig((*big.Int)(txJson.Value))
if overflow {
return nil, fmt.Errorf("value field caused an overflow (uint256)")
}
}
if txJson.GasPrice != nil {
gasPrice, overflow = uint256.FromBig((*big.Int)(txJson.GasPrice))
if overflow {
return nil, fmt.Errorf("gasPrice field caused an overflow (uint256)")
}
}
if txJson.ChainID != nil {
chainId, overflow = uint256.FromBig((*big.Int)(txJson.ChainID))
if overflow {
return nil, fmt.Errorf("chainId field caused an overflow (uint256)")
}
}
switch txJson.Type {
case types.LegacyTxType, types.AccessListTxType:
var toAddr = libcommon.Address{}
if txJson.To != nil {
toAddr = *txJson.To
}
legacyTx := types.NewTransaction(uint64(txJson.Nonce), toAddr, value, uint64(txJson.Gas), gasPrice, txJson.Input)
legacyTx.V.SetFromBig(txJson.V.ToInt())
legacyTx.S.SetFromBig(txJson.S.ToInt())
legacyTx.R.SetFromBig(txJson.R.ToInt())
if txJson.Type == types.AccessListTxType {
accessListTx := types.AccessListTx{
LegacyTx: *legacyTx,
ChainID: chainId,
AccessList: *txJson.Accesses,
}
return &accessListTx, nil
} else {
return legacyTx, nil
}
case types.DynamicFeeTxType:
var tip *uint256.Int
var feeCap *uint256.Int
if txJson.Tip != nil {
tip, overflow = uint256.FromBig((*big.Int)(txJson.Tip))
if overflow {
return nil, fmt.Errorf("maxPriorityFeePerGas field caused an overflow (uint256)")
}
}
if txJson.FeeCap != nil {
feeCap, overflow = uint256.FromBig((*big.Int)(txJson.FeeCap))
if overflow {
return nil, fmt.Errorf("maxFeePerGas field caused an overflow (uint256)")
}
}
dynamicFeeTx := types.DynamicFeeTransaction{
CommonTx: types.CommonTx{
ChainID: chainId,
Nonce: uint64(txJson.Nonce),
To: txJson.To,
Value: value,
Gas: uint64(txJson.Gas),
Data: txJson.Input,
},
Tip: tip,
FeeCap: feeCap,
AccessList: *txJson.Accesses,
}
dynamicFeeTx.V.SetFromBig(txJson.V.ToInt())
dynamicFeeTx.S.SetFromBig(txJson.S.ToInt())
dynamicFeeTx.R.SetFromBig(txJson.R.ToInt())
return &dynamicFeeTx, nil
default:
return nil, nil
}
}
// signUnsignedTransactions converts the input txs to canonical transactions.
//
// The transactions can have two forms, either
// 1. unsigned or
// 2. signed
//
// For (1), r, s, v, need so be zero, and the `secretKey` needs to be set.
// If so, we sign it here and now, with the given `secretKey`
// If the condition above is not met, then it's considered a signed transaction.
//
// To manage this, we read the transactions twice, first trying to read the secretKeys,
// and secondly to read them with the standard tx json format
func signUnsignedTransactions(txs []*txWithKey, signer types.Signer) (types.Transactions, error) {
var signedTxs []types.Transaction
for i, txWithKey := range txs {
tx := txWithKey.tx
key := txWithKey.key
v, r, s := tx.RawSignatureValues()
if key != nil && v.IsZero() && r.IsZero() && s.IsZero() {
// This transaction needs to be signed
signed, err := types.SignTx(tx, signer, key)
if err != nil {
return nil, NewError(ErrorJson, fmt.Errorf("tx %d: failed to sign tx: %v", i, err))
}
signedTxs = append(signedTxs, signed)
} else {
// Already signed
signedTxs = append(signedTxs, tx)
}
}
return signedTxs, nil
}
type Alloc map[libcommon.Address]core.GenesisAccount
func (g Alloc) OnRoot(libcommon.Hash) {}
func (g Alloc) OnAccount(addr libcommon.Address, dumpAccount state.DumpAccount) {
balance, _ := new(big.Int).SetString(dumpAccount.Balance, 10)
var storage map[libcommon.Hash]libcommon.Hash
if dumpAccount.Storage != nil {
storage = make(map[libcommon.Hash]libcommon.Hash)
for k, v := range dumpAccount.Storage {
storage[libcommon.HexToHash(k)] = libcommon.HexToHash(v)
}
}
genesisAccount := core.GenesisAccount{
Code: dumpAccount.Code,
Storage: storage,
Balance: balance,
Nonce: dumpAccount.Nonce,
}
g[addr] = genesisAccount
}
// saveFile marshalls the object to the given file
func saveFile(baseDir, filename string, data interface{}) error {
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err))
}
location := filepath.Join(baseDir, filename)
if err = os.WriteFile(location, b, 0644); err != nil { //nolint:gosec
return NewError(ErrorIO, fmt.Errorf("failed writing output: %v", err))
}
log.Info("Wrote file", "file", location)
return nil
}
// dispatchOutput writes the output data to either stderr or stdout, or to the specified
// files
func dispatchOutput(ctx *cli.Context, baseDir string, result *core.EphemeralExecResult, alloc Alloc, body hexutil.Bytes) error {
stdOutObject := make(map[string]interface{})
stdErrObject := make(map[string]interface{})
dispatch := func(baseDir, fName, name string, obj interface{}) error {
switch fName {
case "stdout":
stdOutObject[name] = obj
case "stderr":
stdErrObject[name] = obj
case "":
// don't save
default: // save to file
if err := saveFile(baseDir, fName, obj); err != nil {
return err
}
}
return nil
}
if err := dispatch(baseDir, ctx.String(OutputAllocFlag.Name), "alloc", alloc); err != nil {
return err
}
if err := dispatch(baseDir, ctx.String(OutputResultFlag.Name), "result", result); err != nil {
return err
}
if err := dispatch(baseDir, ctx.String(OutputBodyFlag.Name), "body", body); 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
}
func NewHeader(env stEnv) *types.Header {
var header types.Header
header.UncleHash = env.ParentUncleHash
header.Coinbase = env.Coinbase
header.Difficulty = env.Difficulty
header.Number = big.NewInt(int64(env.Number))
header.GasLimit = env.GasLimit
header.Time = env.Timestamp
header.BaseFee = env.BaseFee
return &header
}
func CalculateStateRoot(tx kv.RwTx) (*libcommon.Hash, error) {
// Generate hashed state
c, err := tx.RwCursor(kv.PlainState)
if err != nil {
return nil, err
}
h := common.NewHasher()
defer common.ReturnHasherToPool(h)
for k, v, err := c.First(); k != nil; k, v, err = c.Next() {
if err != nil {
return nil, fmt.Errorf("interate over plain state: %w", err)
}
var newK []byte
if len(k) == length.Addr {
newK = make([]byte, length.Hash)
} else {
newK = make([]byte, length.Hash*2+length.Incarnation)
}
h.Sha.Reset()
//nolint:errcheck
h.Sha.Write(k[:length.Addr])
//nolint:errcheck
h.Sha.Read(newK[:length.Hash])
if len(k) > length.Addr {
copy(newK[length.Hash:], k[length.Addr:length.Addr+length.Incarnation])
h.Sha.Reset()
//nolint:errcheck
h.Sha.Write(k[length.Addr+length.Incarnation:])
//nolint:errcheck
h.Sha.Read(newK[length.Hash+length.Incarnation:])
if err = tx.Put(kv.HashedStorage, newK, common.CopyBytes(v)); err != nil {
return nil, fmt.Errorf("insert hashed key: %w", err)
}
} else {
if err = tx.Put(kv.HashedAccounts, newK, common.CopyBytes(v)); err != nil {
return nil, fmt.Errorf("insert hashed key: %w", err)
}
}
}
c.Close()
root, err := trie.CalcRoot("", tx)
if err != nil {
return nil, err
}
return &root, nil
}