diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 578f103885..5c431758ae 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "io/ioutil" + "math/big" "os" "runtime" "sync" @@ -41,6 +42,7 @@ import ( "github.com/scroll-tech/go-ethereum/log" "github.com/scroll-tech/go-ethereum/params" "github.com/scroll-tech/go-ethereum/rlp" + "github.com/scroll-tech/go-ethereum/rollup/fees" "github.com/scroll-tech/go-ethereum/rpc" ) @@ -897,6 +899,16 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex // Run the transaction with tracing enabled. vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true}) + // If gasPrice is 0, make sure that the account has sufficient balance to cover `l1Fee`. + if api.backend.ChainConfig().UsingScroll && message.GasPrice().Cmp(big.NewInt(0)) == 0 { + l1Fee, err := fees.CalculateL1MsgFee(message, vmenv.StateDB) + if err != nil { + return nil, err + } + + statedb.AddBalance(message.From(), l1Fee) + } + // Call Prepare to clear out the statedb access list statedb.Prepare(txctx.TxHash, txctx.TxIndex) diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index 0631706d69..2ca853a163 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -37,6 +37,7 @@ import ( "github.com/scroll-tech/go-ethereum/eth/ethconfig" "github.com/scroll-tech/go-ethereum/node" "github.com/scroll-tech/go-ethereum/params" + "github.com/scroll-tech/go-ethereum/rollup/rcfg" "github.com/scroll-tech/go-ethereum/rpc" ) @@ -182,14 +183,26 @@ func TestToFilterArg(t *testing.T) { } var ( - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - testAddr = crypto.PubkeyToAddress(testKey.PublicKey) - testBalance = big.NewInt(2e15) + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + emptyAccountKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f292") + emptyAddr = crypto.PubkeyToAddress(emptyAccountKey.PublicKey) + testBalance = big.NewInt(2e15) ) var genesis = &core.Genesis{ - Config: params.AllEthashProtocolChanges, - Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}}, + Config: params.AllEthashProtocolChanges, + Alloc: core.GenesisAlloc{ + testAddr: {Balance: testBalance}, + rcfg.L1GasPriceOracleAddress: { + Balance: big.NewInt(0), + Storage: map[common.Hash]common.Hash{ + rcfg.L1BaseFeeSlot: common.BigToHash(big.NewInt(10000)), + rcfg.OverheadSlot: common.BigToHash(big.NewInt(10000)), + rcfg.ScalarSlot: common.BigToHash(big.NewInt(10000)), + }, + }, + }, ExtraData: []byte("test genesis"), Timestamp: 9000, BaseFee: big.NewInt(params.InitialBaseFee), @@ -285,6 +298,9 @@ func TestEthClient(t *testing.T) { "CallContract": { func(t *testing.T) { testCallContract(t, client) }, }, + "EstimateGas": { + func(t *testing.T) { testEstimateGas(t, client) }, + }, "AtFunctions": { func(t *testing.T) { testAtFunctions(t, client) }, }, @@ -534,6 +550,22 @@ func testCallContract(t *testing.T, client *rpc.Client) { } } +func testEstimateGas(t *testing.T, client *rpc.Client) { + ec := NewClient(client) + + // EstimateGas + msg := ethereum.CallMsg{ + From: emptyAddr, + To: &common.Address{}, + GasPrice: big.NewInt(1000000000), + } + _, err := ec.EstimateGas(context.Background(), msg) + + if err == nil || err.Error() != "insufficient funds for l1 fee" { + t.Fatalf("unexpected error: %v", err) + } +} + func testAtFunctions(t *testing.T, client *rpc.Client) { ec := NewClient(client) diff --git a/ethclient/gethclient/gethclient_test.go b/ethclient/gethclient/gethclient_test.go index fc7fc1e7da..cdbd505020 100644 --- a/ethclient/gethclient/gethclient_test.go +++ b/ethclient/gethclient/gethclient_test.go @@ -34,13 +34,16 @@ import ( "github.com/scroll-tech/go-ethereum/ethclient" "github.com/scroll-tech/go-ethereum/node" "github.com/scroll-tech/go-ethereum/params" + "github.com/scroll-tech/go-ethereum/rollup/rcfg" "github.com/scroll-tech/go-ethereum/rpc" ) var ( - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - testAddr = crypto.PubkeyToAddress(testKey.PublicKey) - testBalance = big.NewInt(2e15) + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + emptyAccountKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f292") + emptyAddr = crypto.PubkeyToAddress(emptyAccountKey.PublicKey) + testBalance = big.NewInt(2e15) ) func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { @@ -72,8 +75,18 @@ func generateTestChain() (*core.Genesis, []*types.Block) { db := rawdb.NewMemoryDatabase() config := params.AllEthashProtocolChanges genesis := &core.Genesis{ - Config: config, - Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}}, + Config: config, + Alloc: core.GenesisAlloc{ + testAddr: {Balance: testBalance}, + rcfg.L1GasPriceOracleAddress: { + Balance: big.NewInt(0), + Storage: map[common.Hash]common.Hash{ + rcfg.L1BaseFeeSlot: common.BigToHash(big.NewInt(10000)), + rcfg.OverheadSlot: common.BigToHash(big.NewInt(10000)), + rcfg.ScalarSlot: common.BigToHash(big.NewInt(10000)), + }, + }, + }, ExtraData: []byte("test genesis"), Timestamp: 9000, } @@ -126,6 +139,9 @@ func TestGethClient(t *testing.T) { }, { "TestCallContract", func(t *testing.T) { testCallContract(t, client) }, + }, { + "TestCallContractNoGas", + func(t *testing.T) { testCallContractNoGas(t, client) }, }, } t.Parallel() @@ -306,3 +322,20 @@ func testCallContract(t *testing.T, client *rpc.Client) { t.Fatalf("unexpected error: %v", err) } } + +func testCallContractNoGas(t *testing.T, client *rpc.Client) { + ec := New(client) + msg := ethereum.CallMsg{ + From: emptyAddr, // 0 balance + To: &common.Address{}, + Gas: 21000, + GasPrice: big.NewInt(0), // gas price 0 + Value: big.NewInt(0), + } + + // this would fail with `insufficient funds for gas * price + value` + // before we started considering l1fee for 0 gas calls. + if _, err := ec.CallContract(context.Background(), msg, big.NewInt(0), nil); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 76f0bc0602..8bd4fb5c70 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -47,6 +47,7 @@ import ( "github.com/scroll-tech/go-ethereum/p2p" "github.com/scroll-tech/go-ethereum/params" "github.com/scroll-tech/go-ethereum/rlp" + "github.com/scroll-tech/go-ethereum/rollup/fees" "github.com/scroll-tech/go-ethereum/rpc" ) @@ -848,11 +849,12 @@ func (s *PublicBlockChainAPI) GetStorageAt(ctx context.Context, address common.A // if statDiff is set, all diff will be applied first and then execute the call // message. type OverrideAccount struct { - Nonce *hexutil.Uint64 `json:"nonce"` - Code *hexutil.Bytes `json:"code"` - Balance **hexutil.Big `json:"balance"` - State *map[common.Hash]common.Hash `json:"state"` - StateDiff *map[common.Hash]common.Hash `json:"stateDiff"` + Nonce *hexutil.Uint64 `json:"nonce"` + Code *hexutil.Bytes `json:"code"` + Balance **hexutil.Big `json:"balance"` + BalanceAdd **hexutil.Big `json:"balanceAdd"` + State *map[common.Hash]common.Hash `json:"state"` + StateDiff *map[common.Hash]common.Hash `json:"stateDiff"` } // StateOverride is the collection of overridden accounts. @@ -873,9 +875,16 @@ func (diff *StateOverride) Apply(state *state.StateDB) error { state.SetCode(addr, *account.Code) } // Override account balance. + if account.Balance != nil && account.BalanceAdd != nil { + return fmt.Errorf("account %s has both 'balance' and 'balanceAdd'", addr.Hex()) + } if account.Balance != nil { state.SetBalance(addr, (*big.Int)(*account.Balance)) } + if account.BalanceAdd != nil { + balance := big.NewInt(0).Add(state.GetBalance(addr), (*big.Int)(*account.BalanceAdd)) + state.SetBalance(addr, balance) + } if account.State != nil && account.StateDiff != nil { return fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) } @@ -893,6 +902,54 @@ func (diff *StateOverride) Apply(state *state.StateDB) error { return nil } +func newRPCBalance(balance *big.Int) **hexutil.Big { + rpcBalance := (*hexutil.Big)(balance) + return &rpcBalance +} + +func CalculateL1MsgFee(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64, config *params.ChainConfig) (*big.Int, error) { + if !config.UsingScroll { + return big.NewInt(0), nil + } + + state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if state == nil || err != nil { + return nil, err + } + if err := overrides.Apply(state); err != nil { + return nil, err + } + // 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() + + // Get a new instance of the EVM. + msg, err := args.ToMessage(globalGasCap, header.BaseFee) + if err != nil { + return nil, err + } + evm, _, err := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}) + if err != nil { + return nil, err + } + // Wait for the context to be done and cancel the evm. Even if the + // EVM has finished, cancelling may be done (repeatedly) + go func() { + <-ctx.Done() + evm.Cancel() + }() + + return fees.CalculateL1MsgFee(msg, evm.StateDB) +} + func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) @@ -985,6 +1042,26 @@ func (e *revertError) ErrorData() interface{} { // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. func (s *PublicBlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Bytes, error) { + // If gasPrice is 0 and no state override is set, make sure + // that the account has sufficient balance to cover `l1Fee`. + isGasPriceZero := args.GasPrice == nil || args.GasPrice.ToInt().Cmp(big.NewInt(0)) == 0 + + if overrides == nil { + overrides = &StateOverride{} + } + _, isOverrideSet := (*overrides)[args.from()] + + if isGasPriceZero && !isOverrideSet { + l1Fee, err := CalculateL1MsgFee(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap(), s.b.ChainConfig()) + if err != nil { + return nil, err + } + + (*overrides)[args.from()] = OverrideAccount{ + BalanceAdd: newRPCBalance(l1Fee), + } + } + result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap()) if err != nil { return nil, err @@ -1040,12 +1117,25 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr } balance := state.GetBalance(*args.From) // from can't be nil available := new(big.Int).Set(balance) + + // account for tx value if args.Value != nil { if args.Value.ToInt().Cmp(available) >= 0 { return 0, errors.New("insufficient funds for transfer") } available.Sub(available, args.Value.ToInt()) } + + // account for l1 fee + l1Fee, err := CalculateL1MsgFee(ctx, b, args, blockNrOrHash, nil, 0, gasCap, b.ChainConfig()) + if err != nil { + return 0, err + } + if l1Fee.Cmp(available) >= 0 { + return 0, errors.New("insufficient funds for l1 fee") + } + available.Sub(available, l1Fee) + allowance := new(big.Int).Div(available, feeCap) // If the allowance is larger than maximum uint64, skip checking diff --git a/params/version.go b/params/version.go index 6804f33aa5..ea89c03d23 100644 --- a/params/version.go +++ b/params/version.go @@ -24,7 +24,7 @@ import ( const ( VersionMajor = 3 // Major version component of the current release VersionMinor = 1 // Minor version component of the current release - VersionPatch = 1 // Patch version component of the current release + VersionPatch = 2 // Patch version component of the current release VersionMeta = "alpha" // Version metadata to append to the version string )