diff --git a/core/chain_makers.go b/core/chain_makers.go index 3c43c9d86a..749c8d7704 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -103,7 +103,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti } b.statedb.SetTxContext(tx.Hash(), len(b.txs)) - receipt, err := ApplyTransaction(b.config, bc, &b.header.Coinbase, b.gasPool, b.statedb, b.header, tx, &b.header.GasUsed, vmConfig, nil) + receipt, _, err := ApplyTransaction(b.config, bc, &b.header.Coinbase, b.gasPool, b.statedb, b.header, tx, &b.header.GasUsed, vmConfig, nil) if err != nil { panic(err) diff --git a/core/state/statedb.go b/core/state/statedb.go index 9e680fae61..6b01dbf49a 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -575,6 +575,13 @@ const NoncePath = 2 const CodePath = 3 const SuicidePath = 4 +// PrepareLegacy sets the current transaction hash and index which are +// used when the EVM emits new state logs. +func (s *StateDB) PrepareLegacy(thash common.Hash, ti int) { + s.thash = thash + s.txIndex = ti +} + // GetBalance retrieves the balance from the given address or 0 if object not found func (s *StateDB) GetBalance(addr common.Address) *big.Int { return MVRead(s, blockstm.NewSubpathKey(addr, BalancePath), common.Big0, func(s *StateDB) *big.Int { diff --git a/core/state_processor.go b/core/state_processor.go index 019849e867..fad4200d1d 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -91,7 +91,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg statedb.SetTxContext(tx.Hash(), i) - receipt, err := applyTransaction(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, interruptCtx) + receipt, _, err := applyTransaction(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, interruptCtx) if err != nil { return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } @@ -112,7 +112,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg } // nolint : unparam -func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, interruptCtx context.Context) (*types.Receipt, error) { +func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, interruptCtx context.Context) (*types.Receipt, *ExecutionResult, error) { // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) @@ -133,7 +133,7 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta result, err = ApplyMessageNoFeeBurnOrTip(evm, *msg, gp, interruptCtx) if err != nil { - return nil, err + return nil, nil, err } // stop recording read and write @@ -164,7 +164,7 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta ) if result.Err == vm.ErrInterrupt { - return nil, result.Err + return nil, nil, result.Err } // Update the state with pending changes. @@ -202,21 +202,20 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta receipt.BlockNumber = blockNumber receipt.TransactionIndex = uint(statedb.TxIndex()) - return receipt, err + return receipt, result, err } // ApplyTransaction attempts to apply a transaction to the given state database // and uses the input parameters for its environment. It returns the receipt // for the transaction, gas used and an error if the transaction failed, // indicating the block was invalid. -func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config, interruptCtx context.Context) (*types.Receipt, error) { +func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config, interruptCtx context.Context) (*types.Receipt, *ExecutionResult, error) { msg, err := TransactionToMessage(tx, types.MakeSigner(config, header.Number), header.BaseFee) if err != nil { - return nil, err + return nil, nil, err } // Create a new context to be used in the EVM environment blockContext := NewEVMBlockContext(header, bc, author) vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg) - return applyTransaction(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv, interruptCtx) } diff --git a/eth/backend.go b/eth/backend.go index 42792027c8..61c906c18a 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -370,7 +370,7 @@ func (s *Ethereum) PeerCount() int { // APIs return the collection of RPC services the ethereum package offers. // NOTE, some of these services probably need to be moved to somewhere else. func (s *Ethereum) APIs() []rpc.API { - apis := ethapi.GetAPIs(s.APIBackend) + apis := ethapi.GetAPIs(s.APIBackend, s.blockchain) // Append any APIs exposed explicitly by the consensus engine apis = append(apis, s.engine.APIs(s.BlockChain())...) diff --git a/go.mod b/go.mod index f16c476c74..ecb099ee52 100644 --- a/go.mod +++ b/go.mod @@ -74,6 +74,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/supranational/blst v0.3.11 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 + github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 github.com/tendermint/tendermint v0.34.21 github.com/tyler-smith/go-bip39 v1.1.0 github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa @@ -225,7 +226,6 @@ require ( github.com/streadway/amqp v1.0.0 // indirect github.com/stumble/gorocksdb v0.0.3 // indirect github.com/tendermint/btcd v0.1.1 // indirect - github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 // indirect github.com/tendermint/go-amino v0.16.0 // indirect github.com/tendermint/iavl v0.12.4 // indirect github.com/tendermint/tm-db v0.6.7 // indirect diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 139cf0e394..5318502929 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -21,14 +21,13 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/tendermint/crypto/sha3" "math/big" "runtime" "strings" "time" "github.com/davecgh/go-spew/spew" - "github.com/tyler-smith/go-bip39" - "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -51,6 +50,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" + "github.com/tyler-smith/go-bip39" ) // EthereumAPI provides an API to access Ethereum related information. @@ -653,6 +653,307 @@ func (s *PersonalAccountAPI) Unpair(ctx context.Context, url string, pin string) } } +// AccessListOnState Creates an access list on top of given state +// identical to AccessList, except state is loaded in arguments +func AccessListOnState(ctx context.Context, b Backend, header *types.Header, db *state.StateDB, args TransactionArgs) (acl types.AccessList, gasUsed uint64, vmErr error, err error) { + // If the gas amount is not set, extract this as it will depend on access + // lists, and we'll need to estimate every time + nogas := args.Gas == nil + + // Ensure any missing fields are filled, extract the recipient and input data + if err := args.setDefaults(ctx, b); err != nil { + return nil, 0, nil, err + } + var to common.Address + if args.To != nil { + to = *args.To + } else { + to = crypto.CreateAddress(args.from(), uint64(*args.Nonce)) + } + // Retrieve the precompiles since they don't need to be added to the access list + isPostMerge := header.Difficulty.Cmp(common.Big0) == 0 + // Retrieve the precompiles since they don't need to be added to the access list + precompiles := vm.ActivePrecompiles(b.ChainConfig().Rules(header.Number, isPostMerge, header.Time)) + + // Create an initial tracer + prevTracer := logger.NewAccessListTracer(nil, args.from(), to, precompiles) + if args.AccessList != nil { + prevTracer = logger.NewAccessListTracer(*args.AccessList, args.from(), to, precompiles) + } + for { + // Retrieve the current access list to expand + accessList := prevTracer.AccessList() + log.Trace("Creating access list", "input", accessList) + + // If no gas amount was specified, each unique access list needs its own + // gas calculation. This is quite expensive, but we need to be accurate, + // and it's conversed by the sender only anyway. + if nogas { + args.Gas = nil + if err := args.setDefaults(ctx, b); err != nil { + return nil, 0, nil, err // shouldn't happen, just in case + } + } + + statedb := db.Copy() // woops shouldn't have removed this lol + // Set the accesslist to the last al + args.AccessList = &accessList + msg, err := args.ToMessage(b.RPCGasCap(), header.BaseFee) + if err != nil { + return nil, 0, nil, err + } + + // Apply the transaction with the access list tracer + tracer := logger.NewAccessListTracer(accessList, args.from(), to, precompiles) + config := vm.Config{Tracer: tracer, NoBaseFee: true} + vmenv, _, err := b.GetEVM(ctx, msg, statedb, header, &config) + if err != nil { + return nil, 0, nil, err + } + res, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.GasLimit), nil) + if err != nil { + return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", args.toTransaction().Hash(), err) + } + if tracer.Equal(prevTracer) { + return accessList, res.UsedGas, res.Err, nil + } + prevTracer = tracer + } +} + +// MEVEXEC ADDITIONS +// the following are additional rpc methods added into the execution client for mev searchers + +// BlockChainAPI provides an API to access Ethereum blockchain data. +type SearcherAPI struct { + b Backend + chain *core.BlockChain +} + +func NewSearcherAPI(b Backend, chain *core.BlockChain) *SearcherAPI { + return &SearcherAPI{b, chain} +} + +// CallBundleArgs represents the arguments for a call. +type CallBundleArgs struct { + Txs []hexutil.Bytes `json:"txs"` + BlockNumber rpc.BlockNumber `json:"blockNumber"` + StateBlockNumberOrHash rpc.BlockNumberOrHash `json:"stateBlockNumber"` + Coinbase *string `json:"coinbase"` + Timestamp *uint64 `json:"timestamp"` + Timeout *int64 `json:"timeout"` + GasLimit *uint64 `json:"gasLimit"` + Difficulty *big.Int `json:"difficulty"` + BaseFee *big.Int `json:"baseFee"` + SimulationLogs bool `json:"simulationLogs"` + CreateAccessList bool `json:"createAccessList"` + StateOverrides *StateOverride `json:"stateOverrides"` +} + +// CallBundle will simulate a bundle of transactions at the top of a given block +// number with the state of another (or the same) block. This can be used to +// simulate future blocks with the current state, or it can be used to simulate +// a past block. +// The sender is responsible for signing the transactions and using the correct +// nonce and ensuring validity +func (s *SearcherAPI) CallBundle(ctx context.Context, args CallBundleArgs) (map[string]interface{}, error) { + if len(args.Txs) == 0 { + return nil, errors.New("bundle missing txs") + } + if args.BlockNumber == 0 { + return nil, errors.New("bundle missing blockNumber") + } + + var txs types.Transactions + + for _, encodedTx := range args.Txs { + tx := new(types.Transaction) + if err := tx.UnmarshalBinary(encodedTx); err != nil { + return nil, err + } + txs = append(txs, tx) + } + defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) + + timeoutMilliSeconds := int64(5000) + if args.Timeout != nil { + timeoutMilliSeconds = *args.Timeout + } + timeout := time.Millisecond * time.Duration(timeoutMilliSeconds) + state, parent, err := s.b.StateAndHeaderByNumberOrHash(ctx, args.StateBlockNumberOrHash) + if state == nil || err != nil { + return nil, err + } + if err := args.StateOverrides.Apply(state); err != nil { + return nil, err + } + blockNumber := big.NewInt(int64(args.BlockNumber)) + + timestamp := parent.Time + 1 + if args.Timestamp != nil { + timestamp = *args.Timestamp + } + coinbase := parent.Coinbase + if args.Coinbase != nil { + coinbase = common.HexToAddress(*args.Coinbase) + } + difficulty := parent.Difficulty + if args.Difficulty != nil { + difficulty = args.Difficulty + } + gasLimit := parent.GasLimit + if args.GasLimit != nil { + gasLimit = *args.GasLimit + } + var baseFee *big.Int + if args.BaseFee != nil { + baseFee = args.BaseFee + } else if s.b.ChainConfig().IsLondon(big.NewInt(args.BlockNumber.Int64())) { + baseFee = misc.CalcBaseFee(s.b.ChainConfig(), parent) + } + header := &types.Header{ + ParentHash: parent.Hash(), + Number: blockNumber, + GasLimit: gasLimit, + Time: timestamp, + Difficulty: difficulty, + Coinbase: coinbase, + BaseFee: baseFee, + } + + // Setup context so it may be cancelled the call has completed + // or, in case of unlettered gas, set up a context with a timeout. + var cancel context.CancelFunc + if timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, timeout) + } else { + ctx, cancel = context.WithCancel(ctx) + } + // Make sure the context is cancelled when the call has completed + // this makes sure resources are cleaned up. + defer cancel() + + vmconfig := vm.Config{} + + // Setup the gas pool (also for unmetered requests) + // and apply the message. + gp := new(core.GasPool).AddGas(math.MaxUint64) + + results := []map[string]interface{}{} + coinbaseBalanceBefore := state.GetBalance(coinbase) + + bundleHash := sha3.NewLegacyKeccak256() + signer := types.MakeSigner(s.b.ChainConfig(), blockNumber) + var totalGasUsed uint64 + gasFees := new(big.Int) + for i, tx := range txs { + coinbaseBalanceBeforeTx := state.GetBalance(coinbase) + state.PrepareLegacy(tx.Hash(), i) + + accessListState := state.Copy() // create a copy just in case we use it later for access list creation + + receipt, result, err := core.ApplyTransaction(s.b.ChainConfig(), s.chain, &coinbase, gp, state, header, tx, &header.GasUsed, vmconfig, nil) + if err != nil { + return nil, fmt.Errorf("err: %w; txhash %s", err, tx.Hash()) + } + + txHash := tx.Hash().String() + from, err := types.Sender(signer, tx) + if err != nil { + return nil, fmt.Errorf("err: %w; txhash %s", err, tx.Hash()) + } + to := "0x" + if tx.To() != nil { + to = tx.To().String() + } + jsonResult := map[string]interface{}{ + "txHash": txHash, + "gasUsed": receipt.GasUsed, + "fromAddress": from.String(), + "toAddress": to, + } + totalGasUsed += receipt.GasUsed + gasPrice, err := tx.EffectiveGasTip(header.BaseFee) + if err != nil { + return nil, fmt.Errorf("err: %w; txhash %s", err, tx.Hash()) + } + gasFeesTx := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice) + gasFees.Add(gasFees, gasFeesTx) + bundleHash.Write(tx.Hash().Bytes()) + if result.Err != nil { + jsonResult["error"] = result.Err.Error() + revert := result.Revert() + if len(revert) > 0 { + jsonResult["revert"] = string(revert) + } + } else { + dst := make([]byte, hex.EncodedLen(len(result.Return()))) + hex.Encode(dst, result.Return()) + jsonResult["value"] = "0x" + string(dst) + } + // if simulation logs are requested append it to logs + if args.SimulationLogs { + jsonResult["logs"] = receipt.Logs + } + // if an access list is requested create and append + if args.CreateAccessList { + // ifdk another way to fill all values so this will have to do - x2 + txArgGas := hexutil.Uint64(tx.Gas()) + txArgNonce := hexutil.Uint64(tx.Nonce()) + txArgData := hexutil.Bytes(tx.Data()) + txargs := TransactionArgs{ + From: &from, + To: tx.To(), + Gas: &txArgGas, + Nonce: &txArgNonce, + Data: &txArgData, + Value: (*hexutil.Big)(tx.Value()), + ChainID: (*hexutil.Big)(tx.ChainId()), + } + if tx.GasFeeCap().Cmp(big.NewInt(0)) == 0 { // no maxbasefee, set gasprice instead + txargs.GasPrice = (*hexutil.Big)(tx.GasPrice()) + } else { // otherwise set base and priority fee + txargs.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap()) + txargs.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap()) + } + acl, gasUsed, vmerr, err := AccessListOnState(ctx, s.b, header, accessListState, txargs) + if err == nil { + if gasUsed != receipt.GasUsed { + log.Debug("Gas used in receipt differ from accesslist", "receipt", receipt.GasUsed, "acl", gasUsed) // weird bug but it works + } + if vmerr != nil { + log.Info("CallBundle accesslist creation encountered vmerr", "vmerr", vmerr) + } + jsonResult["accessList"] = acl + + } else { + log.Info("CallBundle accesslist creation encountered err", "err", err) + jsonResult["accessList"] = acl // + } // return the empty accesslist either way + } + coinbaseDiffTx := new(big.Int).Sub(state.GetBalance(coinbase), coinbaseBalanceBeforeTx) + jsonResult["coinbaseDiff"] = coinbaseDiffTx.String() + jsonResult["gasFees"] = gasFeesTx.String() + jsonResult["ethSentToCoinbase"] = new(big.Int).Sub(coinbaseDiffTx, gasFeesTx).String() + jsonResult["gasPrice"] = new(big.Int).Div(coinbaseDiffTx, big.NewInt(int64(receipt.GasUsed))).String() + jsonResult["gasUsed"] = receipt.GasUsed + results = append(results, jsonResult) + } + + ret := map[string]interface{}{} + ret["results"] = results + coinbaseDiff := new(big.Int).Sub(state.GetBalance(coinbase), coinbaseBalanceBefore) + ret["coinbaseDiff"] = coinbaseDiff.String() + ret["gasFees"] = gasFees.String() + ret["ethSentToCoinbase"] = new(big.Int).Sub(coinbaseDiff, gasFees).String() + ret["bundleGasPrice"] = new(big.Int).Div(coinbaseDiff, big.NewInt(int64(totalGasUsed))).String() + ret["totalGasUsed"] = totalGasUsed + ret["stateBlockNumber"] = parent.Number.Int64() + + ret["bundleHash"] = "0x" + common.Bytes2Hex(bundleHash.Sum(nil)) + return ret, nil +} + // BlockChainAPI provides an API to access Ethereum blockchain data. type BlockChainAPI struct { b Backend diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 2dca44cc7e..6c69f9a102 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -19,10 +19,10 @@ package ethapi import ( "context" + "github.com/ethereum/go-ethereum" "math/big" "time" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -114,7 +114,7 @@ type Backend interface { PurgeWhitelistedMilestone() } -func GetAPIs(apiBackend Backend) []rpc.API { +func GetAPIs(apiBackend Backend, chain *core.BlockChain) []rpc.API { nonceLock := new(AddrLocker) return []rpc.API{ @@ -124,6 +124,9 @@ func GetAPIs(apiBackend Backend) []rpc.API { }, { Namespace: "eth", Service: NewBlockChainAPI(apiBackend), + }, { + Namespace: "eth", + Service: NewSearcherAPI(apiBackend, chain), }, { Namespace: "eth", Service: NewTransactionAPI(apiBackend, nonceLock), diff --git a/les/client.go b/les/client.go index 72a74db33e..a446f865e6 100644 --- a/les/client.go +++ b/les/client.go @@ -340,7 +340,7 @@ func (s *LightDummyAPI) Mining() bool { // APIs returns the collection of RPC services the ethereum package offers. // NOTE, some of these services probably need to be moved to somewhere else. func (s *LightEthereum) APIs() []rpc.API { - apis := ethapi.GetAPIs(s.ApiBackend) + apis := ethapi.GetAPIs(s.ApiBackend, nil) apis = append(apis, s.engine.APIs(s.BlockChain().HeaderChain())...) return append(apis, []rpc.API{ diff --git a/miner/worker.go b/miner/worker.go index ae1e92c070..2705b6ba65 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -1033,7 +1033,7 @@ func (w *worker) commitTransaction(env *environment, tx *types.Transaction, inte // nolint : staticcheck interruptCtx = vm.SetCurrentTxOnContext(interruptCtx, tx.Hash()) - receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &env.coinbase, env.gasPool, env.state, env.header, tx, &env.header.GasUsed, *w.chain.GetVMConfig(), interruptCtx) + receipt, _, err := core.ApplyTransaction(w.chainConfig, w.chain, &env.coinbase, env.gasPool, env.state, env.header, tx, &env.header.GasUsed, *w.chain.GetVMConfig(), interruptCtx) if err != nil { env.state.RevertToSnapshot(snap) env.gasPool.SetGas(gp) diff --git a/tests/bor/helper.go b/tests/bor/helper.go index ba9b687d25..b997e7da74 100644 --- a/tests/bor/helper.go +++ b/tests/bor/helper.go @@ -258,7 +258,7 @@ func (b *blockGen) addTxWithChain(bc *core.BlockChain, statedb *state.StateDB, t statedb.SetTxContext(tx.Hash(), len(b.txs)) - receipt, err := core.ApplyTransaction(bc.Config(), bc, &b.header.Coinbase, b.gasPool, statedb, b.header, tx, &b.header.GasUsed, vm.Config{}, nil) + receipt, _, err := core.ApplyTransaction(bc.Config(), bc, &b.header.Coinbase, b.gasPool, statedb, b.header, tx, &b.header.GasUsed, vm.Config{}, nil) if err != nil { panic(err) }