Skip to content

Commit b986f1e

Browse files
authored
feat(API): consider L1 fee in eth_call and eth_estimateGas (#248)
* consider L1 fee in eth_call and eth_estimateGas * address comments * consider l1fee in trace call * nit * fix bugs and add tests * address comments * bump version
1 parent a74b35e commit b986f1e

File tree

5 files changed

+183
-16
lines changed

5 files changed

+183
-16
lines changed

eth/tracers/api.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"errors"
2424
"fmt"
2525
"io/ioutil"
26+
"math/big"
2627
"os"
2728
"runtime"
2829
"sync"
@@ -41,6 +42,7 @@ import (
4142
"github.com/scroll-tech/go-ethereum/log"
4243
"github.com/scroll-tech/go-ethereum/params"
4344
"github.com/scroll-tech/go-ethereum/rlp"
45+
"github.com/scroll-tech/go-ethereum/rollup/fees"
4446
"github.com/scroll-tech/go-ethereum/rpc"
4547
)
4648

@@ -897,6 +899,16 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex
897899
// Run the transaction with tracing enabled.
898900
vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true})
899901

902+
// If gasPrice is 0, make sure that the account has sufficient balance to cover `l1Fee`.
903+
if api.backend.ChainConfig().UsingScroll && message.GasPrice().Cmp(big.NewInt(0)) == 0 {
904+
l1Fee, err := fees.CalculateL1MsgFee(message, vmenv.StateDB)
905+
if err != nil {
906+
return nil, err
907+
}
908+
909+
statedb.AddBalance(message.From(), l1Fee)
910+
}
911+
900912
// Call Prepare to clear out the statedb access list
901913
statedb.Prepare(txctx.TxHash, txctx.TxIndex)
902914

ethclient/ethclient_test.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/scroll-tech/go-ethereum/eth/ethconfig"
3838
"github.com/scroll-tech/go-ethereum/node"
3939
"github.com/scroll-tech/go-ethereum/params"
40+
"github.com/scroll-tech/go-ethereum/rollup/rcfg"
4041
"github.com/scroll-tech/go-ethereum/rpc"
4142
)
4243

@@ -182,14 +183,26 @@ func TestToFilterArg(t *testing.T) {
182183
}
183184

184185
var (
185-
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
186-
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
187-
testBalance = big.NewInt(2e15)
186+
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
187+
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
188+
emptyAccountKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f292")
189+
emptyAddr = crypto.PubkeyToAddress(emptyAccountKey.PublicKey)
190+
testBalance = big.NewInt(2e15)
188191
)
189192

190193
var genesis = &core.Genesis{
191-
Config: params.AllEthashProtocolChanges,
192-
Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}},
194+
Config: params.AllEthashProtocolChanges,
195+
Alloc: core.GenesisAlloc{
196+
testAddr: {Balance: testBalance},
197+
rcfg.L1GasPriceOracleAddress: {
198+
Balance: big.NewInt(0),
199+
Storage: map[common.Hash]common.Hash{
200+
rcfg.L1BaseFeeSlot: common.BigToHash(big.NewInt(10000)),
201+
rcfg.OverheadSlot: common.BigToHash(big.NewInt(10000)),
202+
rcfg.ScalarSlot: common.BigToHash(big.NewInt(10000)),
203+
},
204+
},
205+
},
193206
ExtraData: []byte("test genesis"),
194207
Timestamp: 9000,
195208
BaseFee: big.NewInt(params.InitialBaseFee),
@@ -285,6 +298,9 @@ func TestEthClient(t *testing.T) {
285298
"CallContract": {
286299
func(t *testing.T) { testCallContract(t, client) },
287300
},
301+
"EstimateGas": {
302+
func(t *testing.T) { testEstimateGas(t, client) },
303+
},
288304
"AtFunctions": {
289305
func(t *testing.T) { testAtFunctions(t, client) },
290306
},
@@ -534,6 +550,22 @@ func testCallContract(t *testing.T, client *rpc.Client) {
534550
}
535551
}
536552

553+
func testEstimateGas(t *testing.T, client *rpc.Client) {
554+
ec := NewClient(client)
555+
556+
// EstimateGas
557+
msg := ethereum.CallMsg{
558+
From: emptyAddr,
559+
To: &common.Address{},
560+
GasPrice: big.NewInt(1000000000),
561+
}
562+
_, err := ec.EstimateGas(context.Background(), msg)
563+
564+
if err == nil || err.Error() != "insufficient funds for l1 fee" {
565+
t.Fatalf("unexpected error: %v", err)
566+
}
567+
}
568+
537569
func testAtFunctions(t *testing.T, client *rpc.Client) {
538570
ec := NewClient(client)
539571

ethclient/gethclient/gethclient_test.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ import (
3434
"github.com/scroll-tech/go-ethereum/ethclient"
3535
"github.com/scroll-tech/go-ethereum/node"
3636
"github.com/scroll-tech/go-ethereum/params"
37+
"github.com/scroll-tech/go-ethereum/rollup/rcfg"
3738
"github.com/scroll-tech/go-ethereum/rpc"
3839
)
3940

4041
var (
41-
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
42-
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
43-
testBalance = big.NewInt(2e15)
42+
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
43+
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
44+
emptyAccountKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f292")
45+
emptyAddr = crypto.PubkeyToAddress(emptyAccountKey.PublicKey)
46+
testBalance = big.NewInt(2e15)
4447
)
4548

4649
func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
@@ -72,8 +75,18 @@ func generateTestChain() (*core.Genesis, []*types.Block) {
7275
db := rawdb.NewMemoryDatabase()
7376
config := params.AllEthashProtocolChanges
7477
genesis := &core.Genesis{
75-
Config: config,
76-
Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}},
78+
Config: config,
79+
Alloc: core.GenesisAlloc{
80+
testAddr: {Balance: testBalance},
81+
rcfg.L1GasPriceOracleAddress: {
82+
Balance: big.NewInt(0),
83+
Storage: map[common.Hash]common.Hash{
84+
rcfg.L1BaseFeeSlot: common.BigToHash(big.NewInt(10000)),
85+
rcfg.OverheadSlot: common.BigToHash(big.NewInt(10000)),
86+
rcfg.ScalarSlot: common.BigToHash(big.NewInt(10000)),
87+
},
88+
},
89+
},
7790
ExtraData: []byte("test genesis"),
7891
Timestamp: 9000,
7992
}
@@ -126,6 +139,9 @@ func TestGethClient(t *testing.T) {
126139
}, {
127140
"TestCallContract",
128141
func(t *testing.T) { testCallContract(t, client) },
142+
}, {
143+
"TestCallContractNoGas",
144+
func(t *testing.T) { testCallContractNoGas(t, client) },
129145
},
130146
}
131147
t.Parallel()
@@ -306,3 +322,20 @@ func testCallContract(t *testing.T, client *rpc.Client) {
306322
t.Fatalf("unexpected error: %v", err)
307323
}
308324
}
325+
326+
func testCallContractNoGas(t *testing.T, client *rpc.Client) {
327+
ec := New(client)
328+
msg := ethereum.CallMsg{
329+
From: emptyAddr, // 0 balance
330+
To: &common.Address{},
331+
Gas: 21000,
332+
GasPrice: big.NewInt(0), // gas price 0
333+
Value: big.NewInt(0),
334+
}
335+
336+
// this would fail with `insufficient funds for gas * price + value`
337+
// before we started considering l1fee for 0 gas calls.
338+
if _, err := ec.CallContract(context.Background(), msg, big.NewInt(0), nil); err != nil {
339+
t.Fatalf("unexpected error: %v", err)
340+
}
341+
}

internal/ethapi/api.go

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747
"github.com/scroll-tech/go-ethereum/p2p"
4848
"github.com/scroll-tech/go-ethereum/params"
4949
"github.com/scroll-tech/go-ethereum/rlp"
50+
"github.com/scroll-tech/go-ethereum/rollup/fees"
5051
"github.com/scroll-tech/go-ethereum/rpc"
5152
)
5253

@@ -848,11 +849,12 @@ func (s *PublicBlockChainAPI) GetStorageAt(ctx context.Context, address common.A
848849
// if statDiff is set, all diff will be applied first and then execute the call
849850
// message.
850851
type OverrideAccount struct {
851-
Nonce *hexutil.Uint64 `json:"nonce"`
852-
Code *hexutil.Bytes `json:"code"`
853-
Balance **hexutil.Big `json:"balance"`
854-
State *map[common.Hash]common.Hash `json:"state"`
855-
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"`
852+
Nonce *hexutil.Uint64 `json:"nonce"`
853+
Code *hexutil.Bytes `json:"code"`
854+
Balance **hexutil.Big `json:"balance"`
855+
BalanceAdd **hexutil.Big `json:"balanceAdd"`
856+
State *map[common.Hash]common.Hash `json:"state"`
857+
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"`
856858
}
857859

858860
// StateOverride is the collection of overridden accounts.
@@ -873,9 +875,16 @@ func (diff *StateOverride) Apply(state *state.StateDB) error {
873875
state.SetCode(addr, *account.Code)
874876
}
875877
// Override account balance.
878+
if account.Balance != nil && account.BalanceAdd != nil {
879+
return fmt.Errorf("account %s has both 'balance' and 'balanceAdd'", addr.Hex())
880+
}
876881
if account.Balance != nil {
877882
state.SetBalance(addr, (*big.Int)(*account.Balance))
878883
}
884+
if account.BalanceAdd != nil {
885+
balance := big.NewInt(0).Add(state.GetBalance(addr), (*big.Int)(*account.BalanceAdd))
886+
state.SetBalance(addr, balance)
887+
}
879888
if account.State != nil && account.StateDiff != nil {
880889
return fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex())
881890
}
@@ -893,6 +902,54 @@ func (diff *StateOverride) Apply(state *state.StateDB) error {
893902
return nil
894903
}
895904

905+
func newRPCBalance(balance *big.Int) **hexutil.Big {
906+
rpcBalance := (*hexutil.Big)(balance)
907+
return &rpcBalance
908+
}
909+
910+
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) {
911+
if !config.UsingScroll {
912+
return big.NewInt(0), nil
913+
}
914+
915+
state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
916+
if state == nil || err != nil {
917+
return nil, err
918+
}
919+
if err := overrides.Apply(state); err != nil {
920+
return nil, err
921+
}
922+
// Setup context so it may be cancelled the call has completed
923+
// or, in case of unmetered gas, setup a context with a timeout.
924+
var cancel context.CancelFunc
925+
if timeout > 0 {
926+
ctx, cancel = context.WithTimeout(ctx, timeout)
927+
} else {
928+
ctx, cancel = context.WithCancel(ctx)
929+
}
930+
// Make sure the context is cancelled when the call has completed
931+
// this makes sure resources are cleaned up.
932+
defer cancel()
933+
934+
// Get a new instance of the EVM.
935+
msg, err := args.ToMessage(globalGasCap, header.BaseFee)
936+
if err != nil {
937+
return nil, err
938+
}
939+
evm, _, err := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true})
940+
if err != nil {
941+
return nil, err
942+
}
943+
// Wait for the context to be done and cancel the evm. Even if the
944+
// EVM has finished, cancelling may be done (repeatedly)
945+
go func() {
946+
<-ctx.Done()
947+
evm.Cancel()
948+
}()
949+
950+
return fees.CalculateL1MsgFee(msg, evm.StateDB)
951+
}
952+
896953
func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
897954
defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())
898955

@@ -985,6 +1042,26 @@ func (e *revertError) ErrorData() interface{} {
9851042
// Note, this function doesn't make and changes in the state/blockchain and is
9861043
// useful to execute and retrieve values.
9871044
func (s *PublicBlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Bytes, error) {
1045+
// If gasPrice is 0 and no state override is set, make sure
1046+
// that the account has sufficient balance to cover `l1Fee`.
1047+
isGasPriceZero := args.GasPrice == nil || args.GasPrice.ToInt().Cmp(big.NewInt(0)) == 0
1048+
1049+
if overrides == nil {
1050+
overrides = &StateOverride{}
1051+
}
1052+
_, isOverrideSet := (*overrides)[args.from()]
1053+
1054+
if isGasPriceZero && !isOverrideSet {
1055+
l1Fee, err := CalculateL1MsgFee(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap(), s.b.ChainConfig())
1056+
if err != nil {
1057+
return nil, err
1058+
}
1059+
1060+
(*overrides)[args.from()] = OverrideAccount{
1061+
BalanceAdd: newRPCBalance(l1Fee),
1062+
}
1063+
}
1064+
9881065
result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap())
9891066
if err != nil {
9901067
return nil, err
@@ -1040,12 +1117,25 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
10401117
}
10411118
balance := state.GetBalance(*args.From) // from can't be nil
10421119
available := new(big.Int).Set(balance)
1120+
1121+
// account for tx value
10431122
if args.Value != nil {
10441123
if args.Value.ToInt().Cmp(available) >= 0 {
10451124
return 0, errors.New("insufficient funds for transfer")
10461125
}
10471126
available.Sub(available, args.Value.ToInt())
10481127
}
1128+
1129+
// account for l1 fee
1130+
l1Fee, err := CalculateL1MsgFee(ctx, b, args, blockNrOrHash, nil, 0, gasCap, b.ChainConfig())
1131+
if err != nil {
1132+
return 0, err
1133+
}
1134+
if l1Fee.Cmp(available) >= 0 {
1135+
return 0, errors.New("insufficient funds for l1 fee")
1136+
}
1137+
available.Sub(available, l1Fee)
1138+
10491139
allowance := new(big.Int).Div(available, feeCap)
10501140

10511141
// If the allowance is larger than maximum uint64, skip checking

params/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
const (
2525
VersionMajor = 3 // Major version component of the current release
2626
VersionMinor = 1 // Minor version component of the current release
27-
VersionPatch = 1 // Patch version component of the current release
27+
VersionPatch = 2 // Patch version component of the current release
2828
VersionMeta = "alpha" // Version metadata to append to the version string
2929
)
3030

0 commit comments

Comments
 (0)