diff --git a/core/state_processor.go b/core/state_processor.go index fcaf5a8ff3..61b4bca3b0 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -18,6 +18,8 @@ package core import ( "errors" + "encoding/json" + "fmt" "math/big" @@ -160,3 +162,55 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo vmenv := vm.NewEVM(blockContext, vm.TxContext{BlobHashes: tx.BlobHashes()}, statedb, config, cfg) return applyTransaction(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv) } + +func applyTransactionWithResult(msg *Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, msgTx *Message, usedGas *uint64, evm *vm.EVM, txHash common.Hash) (*types.Receipt, *ExecutionResult, error) { + // Create a new context to be used in the EVM environment. + txContext := NewEVMTxContext(msg) + evm.Reset(txContext, statedb) + + // Apply the transaction to the current state (included in the env). + result, err := ApplyMessage(evm, msg, gp) + if err != nil { + return nil, nil, err + } + + // Update the state with pending changes. + var root []byte + if config.IsByzantium(header.Number) { + // statedb.GetRefund() + + } else { + root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes() + } + *usedGas += result.UsedGas + + // Create a new receipt for the transaction, storing the intermediate root and gas used + // by the tx. + receipt := &types.Receipt{Type: 0, PostState: root, CumulativeGasUsed: *usedGas} + if result.Failed() { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + // receipt.TxHash = tx.Hash() + receipt.GasUsed = result.UsedGas + + // Set the receipt logs and create the bloom filter. + receipt.BlockHash = header.Hash() + receipt.BlockNumber = header.Number + receipt.TransactionIndex = uint(statedb.TxIndex()) + receipt.Logs = statedb.GetLogs(txHash, header.Number.Uint64(), header.Hash()) + return receipt, result, err +} + +func ApplyTransactionWithResult(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, msg *Message, usedGas *uint64, cfg vm.Config, txHash common.Hash) (*types.Receipt, *ExecutionResult, error) { + // 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) + receipt, result, err := applyTransactionWithResult(msg, config, bc, author, gp, statedb, header, msg, usedGas, vmenv, txHash) + return receipt, result, err +} + +type TracerResult interface { + GetResult() (json.RawMessage, error) +} diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index cd57effcb1..e1da155d64 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -582,3 +582,20 @@ func deriveChainId(v *big.Int) *big.Int { v = new(big.Int).Sub(v, big.NewInt(35)) return v.Div(v, big.NewInt(2)) } + +func MakeSigner2(config *params.ChainConfig, blockNumber *big.Int) Signer { + var signer Signer + switch { + case config.IsLondon(blockNumber): + signer = NewLondonSigner(config.ChainID) + case config.IsBerlin(blockNumber): + signer = NewEIP2930Signer(config.ChainID) + case config.IsEIP155(blockNumber): + signer = NewEIP155Signer(config.ChainID) + case config.IsHomestead(blockNumber): + signer = HomesteadSigner{} + default: + signer = FrontierSigner{} + } + return signer +} diff --git a/eth/backend.go b/eth/backend.go index 38c0fa9743..34b1a9b5db 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -299,7 +299,7 @@ func makeExtraData(extra []byte) []byte { // 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/internal/ethapi/api.go b/internal/ethapi/api.go index 52dbaf164c..94ba19eaac 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "math/big" + "strconv" "strings" "time" @@ -49,6 +50,384 @@ import ( "github.com/tyler-smith/go-bip39" ) +type BundleAPI struct { + b Backend + chain *core.BlockChain +} + +func NewBundleAPI(b Backend, chain *core.BlockChain) *BundleAPI { + return &BundleAPI{b, chain} +} + +func (s *BlockChainAPI) CallNew(ctx context.Context, args TransactionArgsBundle, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, blockOverrides *BlockOverrides) (map[string]interface{}, error) { + result, err := DoCallBundle(ctx, s.b, args, blockNrOrHash, overrides, blockOverrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap()) + if err != nil { + return nil, err + } + + return result, nil +} + +func (s *BundleAPI) CallBundle(ctx context.Context, args CallBundleArgs, overrides *StateOverride) (map[string]interface{}, error) { + if len(args.Transactions) == 0 { + return nil, errors.New("bundle missing txs") + } + + if len(args.BlockNumbers) != len(args.Transactions) { + return nil, errors.New("bundle txs and block numbers mismatch") + } + + if len(args.Timestamps) != len(args.Transactions) { + return nil, errors.New("bundle txs and timestamps mismatch") + } + + 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 := overrides.Apply(state); err != nil { + return nil, err + } + + 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.BlockNumbers[0].Int64())) { + baseFee = misc.CalcBaseFee(s.b.ChainConfig(), parent) + } + + // Setup context so it may be cancelled the call has completed + // or, in case of unmetered gas, setup 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{} + vmconfig.NoBaseFee = true + // 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) + + var totalGasUsed uint64 + gasFees := new(big.Int) + for i, tx := range args.Transactions { + header := &types.Header{ + ParentHash: parent.Hash(), + Number: big.NewInt(int64(args.BlockNumbers[i])), + GasLimit: gasLimit, + Time: *args.Timestamps[i], + Difficulty: difficulty, + Coinbase: coinbase, + BaseFee: baseFee, + } + msg, err := tx.ToMessage(0, header.BaseFee) + if err != nil { + return nil, err + } + coinbaseBalanceBeforeTx := state.GetBalance(coinbase) + randomHash := common.HexToHash("0x" + strconv.Itoa(i)) + state.SetTxContext(randomHash, i) + + receipt, result, err := core.ApplyTransactionWithResult(s.b.ChainConfig(), s.chain, &coinbase, gp, state, header, msg, &header.GasUsed, vmconfig, randomHash) + if err != nil { + return nil, fmt.Errorf("err: %w; txhash %s", err, randomHash) + } + + txHash := randomHash.String() + if err != nil { + return nil, fmt.Errorf("err: %w; txhash %s", err, randomHash) + } + logs := receipt.Logs + if logs == nil { + logs = []*types.Log{} + } + jsonResult := map[string]interface{}{ + "txHash": txHash, + "gasUsed": receipt.GasUsed, + "logs": logs, + "status": receipt.Status, + } + totalGasUsed += receipt.GasUsed + 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) + } + coinbaseDiffTx := new(big.Int).Sub(state.GetBalance(coinbase), coinbaseBalanceBeforeTx) + jsonResult["coinbaseDiff"] = coinbaseDiffTx.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() + + return ret, nil +} + +func doCallBundle(ctx context.Context, b Backend, chain *core.BlockChain, args CallBundleArgs, overrides *StateOverride) (map[string]interface{}, error) { + if len(args.Transactions) == 0 { + return nil, errors.New("bundle missing txs") + } + + if len(args.BlockNumbers) != len(args.Transactions) { + return nil, errors.New("bundle txs and block numbers mismatch") + } + + if len(args.Timestamps) != len(args.Transactions) { + return nil, errors.New("bundle txs and timestamps mismatch, len1" + strconv.Itoa(len(args.Timestamps)) + " len2 " + strconv.Itoa(len(args.Transactions))) + } + + 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 := b.StateAndHeaderByNumberOrHash(ctx, args.StateBlockNumberOrHash) + if state == nil || err != nil { + return nil, err + } + + if err := overrides.Apply(state); err != nil { + return nil, err + } + + 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 b.ChainConfig().IsLondon(big.NewInt(args.BlockNumbers[0].Int64())) { + baseFee = misc.CalcBaseFee(b.ChainConfig(), parent) + } + + // Setup context so it may be cancelled the call has completed + // or, in case of unmetered gas, setup 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{} + vmconfig.NoBaseFee = true + // 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) + + var totalGasUsed uint64 + gasFees := new(big.Int) + lastReverted := false + for i, tx := range args.Transactions { + header := &types.Header{ + ParentHash: parent.Hash(), + Number: big.NewInt(int64(args.BlockNumbers[i])), + GasLimit: gasLimit, + Time: *args.Timestamps[i], + Difficulty: difficulty, + Coinbase: coinbase, + BaseFee: baseFee, + } + msg, err := tx.ToMessage(0, header.BaseFee) + if err != nil { + return nil, err + } + coinbaseBalanceBeforeTx := state.GetBalance(coinbase) + randomHash := common.HexToHash("0x" + strconv.Itoa(i)) + state.SetTxContext(randomHash, i) + + receipt, result, err := core.ApplyTransactionWithResult(b.ChainConfig(), chain, &coinbase, gp, state, header, msg, &header.GasUsed, vmconfig, randomHash) + if err != nil { + return nil, fmt.Errorf("err: %w; txhash %s", err, randomHash) + } + + txHash := randomHash.String() + if err != nil { + return nil, fmt.Errorf("err: %w; txhash %s", err, randomHash) + } + logs := receipt.Logs + if logs == nil { + logs = []*types.Log{} + } + jsonResult := map[string]interface{}{ + "txHash": txHash, + "gasUsed": receipt.GasUsed, + "logs": logs, + "status": receipt.Status, + "input": tx.Input, + } + totalGasUsed += receipt.GasUsed + if result.Err != nil { + jsonResult["error"] = result.Err.Error() + revert := result.Revert() + if len(revert) > 0 { + jsonResult["revert"] = string(revert) + } + // if we are last transaction + if i == len(args.Transactions)-1 { + lastReverted = true + } + } else { + dst := make([]byte, hex.EncodedLen(len(result.Return()))) + hex.Encode(dst, result.Return()) + jsonResult["value"] = "0x" + string(dst) + } + coinbaseDiffTx := new(big.Int).Sub(state.GetBalance(coinbase), coinbaseBalanceBeforeTx) + jsonResult["coinbaseDiff"] = coinbaseDiffTx.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["lastReverted"] = lastReverted + return ret, nil +} + +func (s *BundleAPI) CallBundleArray(ctx context.Context, args []CallBundleArgs, overrides *StateOverride) ([]map[string]interface{}, error) { + ret := []map[string]interface{}{} + for _, arg := range args { + result, err := doCallBundle(ctx, s.b, s.chain, arg, overrides) + if err != nil { + return nil, err + } + ret = append(ret, result) + } + return ret, nil +} + +func (s *BundleAPI) SearchMaxWallet(ctx context.Context, args MaxWalletSearchArgs, overrides *StateOverride) (map[string]interface{}, error) { + timeoutMilliSeconds := int64(5000) + timeout := time.Millisecond * time.Duration(timeoutMilliSeconds) + 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() + currentPercentage := 50000 // 100000 = 100% + lower := 0 + upper := 100000 + resultPercentage := 0 + lastResult := map[string]interface{}{} + lastPercentage := 0 + for { + if (lastPercentage == currentPercentage) || (currentPercentage <= 1) { + resultPercentage = currentPercentage + break + } + // convert percentage to hex and pad with 0s until 64 chars + percentageHex := fmt.Sprintf("%064s", strconv.FormatInt(int64(currentPercentage), 16)) + originalFirst10Chars := args.MaxWalletTransaction.Data.String()[0:10] + m := hexutil.Bytes{} + m.UnmarshalText([]byte(originalFirst10Chars + percentageHex)) + + args.MaxWalletTransaction.setInput(m) + callBundleArgs := CallBundleArgs{ + Transactions: append(args.Transactions, args.MaxWalletTransaction), + BlockNumbers: append(args.BlockNumbers, args.MaxWalletBlockNumber), + Timestamps: append(args.Timestamps, args.MaxWalletTimestamp), + StateBlockNumberOrHash: args.StateBlockNumberOrHash, + } + result, err := doCallBundle(ctx, s.b, s.chain, callBundleArgs, overrides) + lastResult = result + if err != nil { + return lastResult, err + } + // if results last tx reverted, we found the max wallet + if result["lastReverted"] == true && currentPercentage <= 1 { + resultPercentage = 0 + break + } + if currentPercentage >= 90000 { + resultPercentage = 90000 + break + } + if result["lastReverted"] == true { + upper = currentPercentage + } else { + lower = currentPercentage + } + lastPercentage = currentPercentage + currentPercentage = int((upper + lower) / 2) + } + lastResult["percentage"] = resultPercentage + return lastResult, nil +} + // EthereumAPI provides an API to access Ethereum related information. type EthereumAPI struct { b Backend @@ -2171,4 +2550,4 @@ func toHexSlice(b [][]byte) []string { r[i] = hexutil.Encode(b[i]) } return r -} +} \ No newline at end of file diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 458fb811ed..0333020c38 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -99,7 +99,7 @@ type Backend interface { ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) } -func GetAPIs(apiBackend Backend) []rpc.API { +func GetAPIs(apiBackend Backend, chain *core.BlockChain) []rpc.API { nonceLock := new(AddrLocker) return []rpc.API{ { @@ -123,6 +123,11 @@ func GetAPIs(apiBackend Backend) []rpc.API { }, { Namespace: "personal", Service: NewPersonalAccountAPI(apiBackend, nonceLock), + }, { + Namespace: "eth", + Version: "1.0", + Service: NewBundleAPI(apiBackend, chain), + Public: true, }, } } diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index e4cf81a3f4..dfe2465285 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -55,6 +55,62 @@ type TransactionArgs struct { ChainID *hexutil.Big `json:"chainId,omitempty"` } +type TransactionArgsBundle struct { + Transactions1 []TransactionArgs `json:"transactions1"` + Transactions2 []TransactionArgs `json:"transactions2"` + BlockNumbers1 []rpc.BlockNumber `json:"blockNumbers1"` + BlockNumbers2 []rpc.BlockNumber `json:"blockNumbers2"` +} + +type MaxWalletSearchArgs struct { + Transactions []TransactionArgs `json:"transactions"` + MaxWalletTransaction TransactionArgs `json:"maxWalletTransaction"` + BlockNumbers []rpc.BlockNumber `json:"blockNumbers"` + MaxWalletBlockNumber rpc.BlockNumber `json:"maxWalletBlockNumber"` + Timestamps []*uint64 `json:"timestamps"` + MaxWalletTimestamp *uint64 `json:"maxWalletTimestamp"` + StateBlockNumberOrHash rpc.BlockNumberOrHash `json:"stateBlockNumber"` +} + +type CallBundleArgs struct { + Transactions []TransactionArgs `json:"transactions"` + BlockNumbers []rpc.BlockNumber `json:"blockNumbers"` + Timestamps []*uint64 `json:"timestamps"` + StateBlockNumberOrHash rpc.BlockNumberOrHash `json:"stateBlockNumber"` + Coinbase *string `json:"coinbase"` + Timeout *int64 `json:"timeout"` + GasLimit *uint64 `json:"gasLimit"` + Difficulty *big.Int `json:"difficulty"` + BaseFee *big.Int `json:"baseFee"` +} + +type TransactionArgs2 struct { + From *common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` + MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"` + Value *hexutil.Big `json:"value"` + Nonce *hexutil.Uint64 `json:"nonce"` + Data *hexutil.Bytes `json:"data"` + Input *hexutil.Bytes `json:"input"` + AccessList *types.AccessList `json:"accessList,omitempty"` + ChainID *hexutil.Big `json:"chainId,omitempty"` + From1 *common.Address `json:"from1"` + To1 *common.Address `json:"to1"` + Gas1 *hexutil.Uint64 `json:"gas1"` + GasPrice1 *hexutil.Big `json:"gasPrice1"` + MaxFeePerGas1 *hexutil.Big `json:"maxFeePerGas1"` + MaxPriorityFeePerGas1 *hexutil.Big `json:"maxPriorityFeePerGas1"` + Value1 *hexutil.Big `json:"value1"` + Nonce1 *hexutil.Uint64 `json:"nonce1"` + Data1 *hexutil.Bytes `json:"data1"` + Input1 *hexutil.Bytes `json:"input1"` + AccessList1 *types.AccessList `json:"accessList1,omitempty"` + ChainID1 *hexutil.Big `json:"chainId1,omitempty"` +} + // from retrieves the transaction sender address. func (args *TransactionArgs) from() common.Address { if args.From == nil { @@ -74,6 +130,10 @@ func (args *TransactionArgs) data() []byte { return nil } +func (args *TransactionArgs) setInput(data []byte) { + args.Input = (*hexutil.Bytes)(&data) +} + // setDefaults fills in default values for unspecified tx fields. func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error { if err := args.setFeeDefaults(ctx, b); err != nil { diff --git a/les/client.go b/les/client.go index be5e9fd564..629c04858b 100644 --- a/les/client.go +++ b/les/client.go @@ -288,7 +288,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{ {