Skip to content

Commit

Permalink
eth_estimateGas CIP-64 and CIP-66 compatibility (#91)
Browse files Browse the repository at this point in the history
* rpc: include feeCurrency in transaction-args

This commit fixes wrong gas calculation in `eth_estimateGas` calls,
when the additional `feeCurrency` parameter is used.

The TransactionArgs struct is used in transaction related endpoints like
`eth_sendTransaction`, `eth_signTransaction`, `eth_estimateGas` and many
more.

CIP-64 and CIP-66 transaction types make use of an additional transaction
parameter `feeCurrency` and some client libraries are already sending
this in the RPC request body, however the remote procedures omitted
this during unmarshaling and the value was never passed to the EVM.

Now the TransactionArgs struct includes an optional FeeCurrency field
for correct unmarshaling, and the field is passed along downstream
when constructing EVM messages out of the struct.
This e.g. allows gas estimation to consider the different intrinsic
gas for transactions paid in non-native token.

* Rename celoapi file

* Add Backend wrapper for Celo functionality

* Make transaction-args CIP-64/66 compatible

* Make eth_estimateGas CIP64 and CIP66 compatible

* Move error message inside function
ezdac authored and karlb committed Aug 30, 2024
1 parent b946814 commit 03b5006
Showing 15 changed files with 410 additions and 98 deletions.
12 changes: 11 additions & 1 deletion core/state_transition.go
Original file line number Diff line number Diff line change
@@ -184,6 +184,8 @@ type Message struct {
// `nil` corresponds to CELO (native currency).
// All other values should correspond to ERC20 contract addresses.
FeeCurrency *common.Address

MaxFeeInFeeCurrency *big.Int // MaxFeeInFeeCurrency is the maximum fee that can be charged in the fee currency.
}

// TransactionToMessage converts a transaction into a Message.
@@ -207,7 +209,8 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In
BlobHashes: tx.BlobHashes(),
BlobGasFeeCap: tx.BlobGasFeeCap(),

FeeCurrency: tx.FeeCurrency(),
FeeCurrency: tx.FeeCurrency(),
MaxFeeInFeeCurrency: nil, // Will only be set once CIP-66 is implemented
}
// If baseFee provided, set gasPrice to effectiveGasPrice.
if baseFee != nil {
@@ -225,6 +228,13 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In
return msg, err
}

// IsFeeCurrencyDenominated returns whether the gas-price related
// fields are denominated in a given fee currency or in the native token.
// This effectively is only true for CIP-64 transactions.
func (msg *Message) IsFeeCurrencyDenominated() bool {
return msg.FeeCurrency != nil && msg.MaxFeeInFeeCurrency == nil
}

// ApplyMessage computes the new state by applying the given message
// against the old state within the environment.
//
17 changes: 17 additions & 0 deletions e2e_test/js-tests/test_viem_tx.mjs
Original file line number Diff line number Diff line change
@@ -181,6 +181,23 @@ describe("viem send tx", () => {
assert.equal(request.maxPriorityFeePerGas, fees.maxPriorityFeePerGas);
}).timeout(10_000);

it("send fee currency with gas estimation tx and check receipt", async () => {
const request = await walletClient.prepareTransactionRequest({
account,
to: "0x00000000000000000000000000000000DeaDBeef",
value: 2,
feeCurrency: process.env.FEE_CURRENCY,
maxFeePerGas: 2000000000n,
maxPriorityFeePerGas: 0n,
});
const signature = await walletClient.signTransaction(request);
const hash = await walletClient.sendRawTransaction({
serializedTransaction: signature,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
assert.equal(receipt.status, "success", "receipt status 'failure'");
}).timeout(10_000);

it("send overlapping nonce tx in different currencies", async () => {
const priceBump = 1.1;
const rate = 2;
5 changes: 3 additions & 2 deletions eth/backend.go
Original file line number Diff line number Diff line change
@@ -373,7 +373,8 @@ 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)
celoBackend := celoapi.NewCeloAPIBackend(s.APIBackend)
apis := ethapi.GetAPIs(celoBackend)

// Append any APIs exposed explicitly by the consensus engine
apis = append(apis, s.engine.APIs(s.BlockChain())...)
@@ -401,7 +402,7 @@ func (s *Ethereum) APIs() []rpc.API {
// on the eth namespace, this will overwrite the original procedures.
{
Namespace: "eth",
Service: celoapi.NewCeloAPI(s, s.APIBackend),
Service: celoapi.NewCeloAPI(s, celoBackend),
},
}...)
}
36 changes: 27 additions & 9 deletions eth/gasestimator/gasestimator.go
Original file line number Diff line number Diff line change
@@ -20,10 +20,11 @@ import (
"context"
"errors"
"fmt"
"math"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/exchange"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
@@ -49,7 +50,7 @@ type Options struct {
// Estimate returns the lowest possible gas limit that allows the transaction to
// run successfully with the provided context options. It returns an error if the
// transaction would always revert, or if there are unexpected failures.
func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uint64) (uint64, []byte, error) {
func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uint64, exchangeRates common.ExchangeRates, balance *big.Int) (uint64, []byte, error) {
// Binary search the gas limit, as it may need to be higher than the amount used
var (
lo uint64 // lowest-known gas limit where tx execution fails
@@ -71,14 +72,29 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
}
// Recap the highest gas limit with account's available balance.
if feeCap.BitLen() != 0 {
balance := opts.State.GetBalance(call.From).ToBig()

available := balance
if call.Value != nil {
if call.Value.Cmp(available) >= 0 {
return 0, nil, core.ErrInsufficientFundsForTransfer
if call.FeeCurrency != nil {
if !call.IsFeeCurrencyDenominated() {
// CIP-66, prices are given in native token.
// We need to check the allowance in the converted feeCurrency
var err error
feeCap, err = exchange.ConvertCeloToCurrency(exchangeRates, call.FeeCurrency, feeCap)
if err != nil {
return 0, nil, err
}
}
} else {
if call.Value != nil {
if call.Value.Cmp(available) >= 0 {
return 0, nil, core.ErrInsufficientFundsForTransfer
}
available.Sub(available, call.Value)
}
available.Sub(available, call.Value)
}

// cap the available by the maxFeeInFeeCurrency
if call.MaxFeeInFeeCurrency != nil {
available = math.BigMin(available, call.MaxFeeInFeeCurrency)
}
if opts.Config.IsCancun(opts.Header.Number, opts.Header.Time) && len(call.BlobHashes) > 0 {
blobGasPerBlob := new(big.Int).SetInt64(params.BlobTxBlobGasPerBlob)
@@ -99,7 +115,9 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
transfer = new(big.Int)
}
log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance,
"sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance)
"sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance,
"feeCurrency", call.FeeCurrency, "maxFeeInFeeCurrency", call.MaxFeeInFeeCurrency,
)
hi = allowance.Uint64()
}
}
2 changes: 1 addition & 1 deletion eth/tracers/api.go
Original file line number Diff line number Diff line change
@@ -994,7 +994,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
return nil, err
}
var (
msg = args.ToMessage(vmctx.BaseFee)
msg = args.ToMessage(vmctx.BaseFee, vmctx.ExchangeRates)
tx = args.ToTransaction()
traceConfig *TraceConfig
)
2 changes: 1 addition & 1 deletion graphql/graphql.go
Original file line number Diff line number Diff line change
@@ -1282,7 +1282,7 @@ func (p *Pending) EstimateGas(ctx context.Context, args struct {

// Resolver is the top-level object in the GraphQL hierarchy.
type Resolver struct {
backend ethapi.Backend
backend ethapi.CeloBackend
filterSystem *filters.FilterSystem
}

4 changes: 3 additions & 1 deletion graphql/graphql_test.go
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ import (
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/filters"
"github.com/ethereum/go-ethereum/internal/celoapi"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"

@@ -479,7 +480,8 @@ func newGQLService(t *testing.T, stack *node.Node, shanghai bool, gspec *core.Ge
}
// Set up handler
filterSystem := filters.NewFilterSystem(ethBackend.APIBackend, filters.Config{})
handler, err := newHandler(stack, ethBackend.APIBackend, filterSystem, []string{}, []string{})
celoBackend := celoapi.NewCeloAPIBackend(ethBackend.APIBackend)
handler, err := newHandler(stack, celoBackend, filterSystem, []string{}, []string{})
if err != nil {
t.Fatalf("could not create graphql service: %v", err)
}
6 changes: 4 additions & 2 deletions graphql/service.go
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ import (
"time"

"github.com/ethereum/go-ethereum/eth/filters"
"github.com/ethereum/go-ethereum/internal/celoapi"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
@@ -107,13 +108,14 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

// New constructs a new GraphQL service instance.
func New(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) error {
_, err := newHandler(stack, backend, filterSystem, cors, vhosts)
celoBackend := celoapi.NewCeloAPIBackend(backend)
_, err := newHandler(stack, celoBackend, filterSystem, cors, vhosts)
return err
}

// newHandler returns a new `http.Handler` that will answer GraphQL queries.
// It additionally exports an interactive query browser on the / endpoint.
func newHandler(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) (*handler, error) {
func newHandler(stack *node.Node, backend ethapi.CeloBackend, filterSystem *filters.FilterSystem, cors, vhosts []string) (*handler, error) {
q := Resolver{backend, filterSystem}

s, err := graphql.ParseSchema(schema, &q)
97 changes: 97 additions & 0 deletions internal/celoapi/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package celoapi

import (
"context"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/exchange"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/contracts"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/internal/ethapi"
)

type Ethereum interface {
BlockChain() *core.BlockChain
}

type CeloAPI struct {
ethAPI *ethapi.EthereumAPI
eth Ethereum
}

func NewCeloAPI(e Ethereum, b ethapi.CeloBackend) *CeloAPI {
return &CeloAPI{
ethAPI: ethapi.NewEthereumAPI(b),
eth: e,
}
}

func (c *CeloAPI) convertedCurrencyValue(v *hexutil.Big, feeCurrency *common.Address) (*hexutil.Big, error) {
if feeCurrency != nil {
convertedTipCap, err := c.convertCeloToCurrency(v.ToInt(), feeCurrency)
if err != nil {
return nil, fmt.Errorf("convert to feeCurrency: %w", err)
}
v = (*hexutil.Big)(convertedTipCap)
}
return v, nil
}

func (c *CeloAPI) celoBackendCurrentState() (*contracts.CeloBackend, error) {
state, err := c.eth.BlockChain().State()
if err != nil {
return nil, fmt.Errorf("retrieve HEAD blockchain state': %w", err)
}

cb := &contracts.CeloBackend{
ChainConfig: c.eth.BlockChain().Config(),
State: state,
}
return cb, nil
}

func (c *CeloAPI) convertCeloToCurrency(nativePrice *big.Int, feeCurrency *common.Address) (*big.Int, error) {
cb, err := c.celoBackendCurrentState()
if err != nil {
return nil, err
}
er, err := contracts.GetExchangeRates(cb)
if err != nil {
return nil, fmt.Errorf("retrieve exchange rates from current state: %w", err)
}
return exchange.ConvertCeloToCurrency(er, feeCurrency, nativePrice)
}

// GasPrice wraps the original JSON RPC `eth_gasPrice` and adds an additional
// optional parameter `feeCurrency` for fee-currency conversion.
// When `feeCurrency` is not given, then the original JSON RPC method is called without conversion.
func (c *CeloAPI) GasPrice(ctx context.Context, feeCurrency *common.Address) (*hexutil.Big, error) {
tipcap, err := c.ethAPI.GasPrice(ctx)
if err != nil {
return nil, err
}
// Between the call to `ethapi.GasPrice` and the call to fetch and convert the rates,
// there is a chance of a state-change. This means that gas-price suggestion is calculated
// based on state of block x, while the currency conversion could be calculated based on block
// x+1.
// However, a similar race condition is present in the `ethapi.GasPrice` method itself.
return c.convertedCurrencyValue(tipcap, feeCurrency)
}

// MaxPriorityFeePerGas wraps the original JSON RPC `eth_maxPriorityFeePerGas` and adds an additional
// optional parameter `feeCurrency` for fee-currency conversion.
// When `feeCurrency` is not given, then the original JSON RPC method is called without conversion.
func (c *CeloAPI) MaxPriorityFeePerGas(ctx context.Context, feeCurrency *common.Address) (*hexutil.Big, error) {
tipcap, err := c.ethAPI.MaxPriorityFeePerGas(ctx)
if err != nil {
return nil, err
}
// Between the call to `ethapi.MaxPriorityFeePerGas` and the call to fetch and convert the rates,
// there is a chance of a state-change. This means that gas-price suggestion is calculated
// based on state of block x, while the currency conversion could be calculated based on block
// x+1.
return c.convertedCurrencyValue(tipcap, feeCurrency)
}
106 changes: 51 additions & 55 deletions internal/celoapi/backend.go
Original file line number Diff line number Diff line change
@@ -7,91 +7,87 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/exchange"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/contracts"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/rpc"
)

type Ethereum interface {
BlockChain() *core.BlockChain
func NewCeloAPIBackend(b ethapi.Backend) *CeloAPIBackend {
return &CeloAPIBackend{
Backend: b,
exchangeRatesCache: lru.NewCache[common.Hash, common.ExchangeRates](128),
}
}

type CeloAPI struct {
ethAPI *ethapi.EthereumAPI
eth Ethereum
}
// CeloAPIBackend is a wrapper for the ethapi.Backend, that provides additional Celo specific
// functionality. CeloAPIBackend is mainly passed to the JSON RPC services and provides
// an easy way to make extra functionality available in the service internal methods without
// having to change their call signature significantly.
// CeloAPIBackend keeps a threadsafe LRU cache of block-hash to exchange rates for that block.
// Cache invalidation is only a problem when an already existing blocks' hash
// doesn't change, but the rates change. That shouldn't be possible, since changing the rates
// requires different transaction hashes / state and thus a different block hash.
// If the previous rates change during a reorg, the previous block hash should also change
// and with it the new block's hash.
// Stale branches cache values will get evicted eventually.
type CeloAPIBackend struct {
ethapi.Backend

func NewCeloAPI(e Ethereum, b ethapi.Backend) *CeloAPI {
return &CeloAPI{
ethAPI: ethapi.NewEthereumAPI(b),
eth: e,
}
exchangeRatesCache *lru.Cache[common.Hash, common.ExchangeRates]
}

func (c *CeloAPI) convertedCurrencyValue(v *hexutil.Big, feeCurrency *common.Address) (*hexutil.Big, error) {
if feeCurrency != nil {
convertedTipCap, err := c.convertGoldToCurrency(v.ToInt(), feeCurrency)
if err != nil {
return nil, fmt.Errorf("convert to feeCurrency: %w", err)
}
v = (*hexutil.Big)(convertedTipCap)
func (b *CeloAPIBackend) getContractCaller(ctx context.Context, atBlock common.Hash) (*contracts.CeloBackend, error) {
state, _, err := b.Backend.StateAndHeaderByNumberOrHash(
ctx,
rpc.BlockNumberOrHashWithHash(atBlock, false),
)
if err != nil {
return nil, fmt.Errorf("retrieve state for block hash %s: %w", atBlock.String(), err)
}
return v, nil
return &contracts.CeloBackend{
ChainConfig: b.Backend.ChainConfig(),
State: state,
}, nil
}

func (c *CeloAPI) celoBackendCurrentState() (*contracts.CeloBackend, error) {
state, err := c.eth.BlockChain().State()
func (b *CeloAPIBackend) GetFeeBalance(ctx context.Context, atBlock common.Hash, account common.Address, feeCurrency *common.Address) (*big.Int, error) {
cb, err := b.getContractCaller(ctx, atBlock)
if err != nil {
return nil, fmt.Errorf("retrieve HEAD blockchain state': %w", err)
}

cb := &contracts.CeloBackend{
ChainConfig: c.eth.BlockChain().Config(),
State: state,
return nil, err
}
return cb, nil
return contracts.GetFeeBalance(cb, account, feeCurrency), nil
}

func (c *CeloAPI) convertGoldToCurrency(nativePrice *big.Int, feeCurrency *common.Address) (*big.Int, error) {
cb, err := c.celoBackendCurrentState()
func (b *CeloAPIBackend) GetExchangeRates(ctx context.Context, atBlock common.Hash) (common.ExchangeRates, error) {
cachedRates, ok := b.exchangeRatesCache.Get(atBlock)
if ok {
return cachedRates, nil
}
cb, err := b.getContractCaller(ctx, atBlock)
if err != nil {
return nil, err
}
er, err := contracts.GetExchangeRates(cb)
if err != nil {
return nil, fmt.Errorf("retrieve exchange rates from current state: %w", err)
return nil, err
}
return exchange.ConvertCeloToCurrency(er, feeCurrency, nativePrice)
b.exchangeRatesCache.Add(atBlock, er)
return er, nil
}

// GasPrice wraps the original JSON RPC `eth_gasPrice` and adds an additional
// optional parameter `feeCurrency` for fee-currency conversion.
// When `feeCurrency` is not given, then the original JSON RPC method is called without conversion.
func (c *CeloAPI) GasPrice(ctx context.Context, feeCurrency *common.Address) (*hexutil.Big, error) {
tipcap, err := c.ethAPI.GasPrice(ctx)
func (b *CeloAPIBackend) ConvertToCurrency(ctx context.Context, atBlock common.Hash, value *big.Int, fromFeeCurrency *common.Address) (*big.Int, error) {
er, err := b.GetExchangeRates(ctx, atBlock)
if err != nil {
return nil, err
}
// Between the call to `ethapi.GasPrice` and the call to fetch and convert the rates,
// there is a chance of a state-change. This means that gas-price suggestion is calculated
// based on state of block x, while the currency conversion could be calculated based on block
// x+1.
// However, a similar race condition is present in the `ethapi.GasPrice` method itself.
return c.convertedCurrencyValue(tipcap, feeCurrency)
return exchange.ConvertCeloToCurrency(er, fromFeeCurrency, value)
}

// MaxPriorityFeePerGas wraps the original JSON RPC `eth_maxPriorityFeePerGas` and adds an additional
// optional parameter `feeCurrency` for fee-currency conversion.
// When `feeCurrency` is not given, then the original JSON RPC method is called without conversion.
func (c *CeloAPI) MaxPriorityFeePerGas(ctx context.Context, feeCurrency *common.Address) (*hexutil.Big, error) {
tipcap, err := c.ethAPI.MaxPriorityFeePerGas(ctx)
func (b *CeloAPIBackend) ConvertToGold(ctx context.Context, atBlock common.Hash, value *big.Int, toFeeCurrency *common.Address) (*big.Int, error) {
er, err := b.GetExchangeRates(ctx, atBlock)
if err != nil {
return nil, err
}
// Between the call to `ethapi.MaxPriorityFeePerGas` and the call to fetch and convert the rates,
// there is a chance of a state-change. This means that gas-price suggestion is calculated
// based on state of block x, while the currency conversion could be calculated based on block
// x+1.
return c.convertedCurrencyValue(tipcap, feeCurrency)
return exchange.ConvertCurrencyToCelo(er, value, toFeeCurrency)
}
55 changes: 42 additions & 13 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
@@ -58,6 +58,7 @@ import (
const estimateGasErrorRatio = 0.015

var errBlobTxNotSupported = errors.New("signing blob transactions not supported")
var emptyExchangeRates = make(common.ExchangeRates)

// EthereumAPI provides an API to access Ethereum related information.
type EthereumAPI struct {
@@ -303,11 +304,11 @@ func (api *EthereumAccountAPI) Accounts() []common.Address {
type PersonalAccountAPI struct {
am *accounts.Manager
nonceLock *AddrLocker
b Backend
b CeloBackend
}

// NewPersonalAccountAPI creates a new PersonalAccountAPI.
func NewPersonalAccountAPI(b Backend, nonceLock *AddrLocker) *PersonalAccountAPI {
func NewPersonalAccountAPI(b CeloBackend, nonceLock *AddrLocker) *PersonalAccountAPI {
return &PersonalAccountAPI{
am: b.AccountManager(),
nonceLock: nonceLock,
@@ -637,11 +638,11 @@ func (api *PersonalAccountAPI) Unpair(ctx context.Context, url string, pin strin

// BlockChainAPI provides an API to access Ethereum blockchain data.
type BlockChainAPI struct {
b Backend
b CeloBackend
}

// NewBlockChainAPI creates a new Ethereum blockchain API.
func NewBlockChainAPI(b Backend) *BlockChainAPI {
func NewBlockChainAPI(b CeloBackend) *BlockChainAPI {
return &BlockChainAPI{b}
}

@@ -1202,7 +1203,7 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S
if err := args.CallDefaults(globalGasCap, blockCtx.BaseFee, b.ChainConfig().ChainID); err != nil {
return nil, err
}
msg := args.ToMessage(blockCtx.BaseFee)
msg := args.ToMessage(blockCtx.BaseFee, blockCtx.ExchangeRates)
evm := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}, &blockCtx)

// Wait for the context to be done and cancel the evm. Even if the
@@ -1229,7 +1230,7 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S
return result, nil
}

func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, blockOverrides *BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
func DoCall(ctx context.Context, b CeloBackend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, blockOverrides *BlockOverrides, 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())

state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
@@ -1285,7 +1286,7 @@ func (api *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockN
// successfully at block `blockNrOrHash`. It returns error if the transaction would revert, or if
// there are unexpected failures. The gas limit is capped by both `args.Gas` (if non-nil &
// non-zero) and `gasCap` (if non-zero).
func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, gasCap uint64) (hexutil.Uint64, error) {
func DoEstimateGas(ctx context.Context, b CeloBackend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, gasCap uint64) (hexutil.Uint64, error) {
// Retrieve the base state and mutate it with any overrides
state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
@@ -1310,10 +1311,28 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID); err != nil {
return 0, err
}
call := args.ToMessage(header.BaseFee)

// Celo specific: get exchange rates if fee currency is specified
exchangeRates := emptyExchangeRates
if args.FeeCurrency != nil {
// It is debatable whether we should use Hash or ParentHash here. Usually,
// user would probably like the recent rates after the block, so we use Hash.
exchangeRates, err = b.GetExchangeRates(ctx, header.Hash())
if err != nil {
return 0, fmt.Errorf("get exchange rates for block: %v err: %w", header.Hash(), err)
}
}

call := args.ToMessage(header.BaseFee, exchangeRates)

// Celo specific: get balance
balance, err := b.GetFeeBalance(ctx, opts.Header.Hash(), call.From, args.FeeCurrency)
if err != nil {
return 0, err
}

// Run the gas estimation and wrap any revertals into a custom return
estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap)
estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap, exchangeRates, balance)
if err != nil {
if len(revert) > 0 {
return 0, newRevertError(revert)
@@ -1690,7 +1709,7 @@ func (api *BlockChainAPI) CreateAccessList(ctx context.Context, args Transaction
// AccessList creates an access list for the given transaction.
// If the accesslist creation fails an error is returned.
// If the transaction itself fails, an vmErr is returned.
func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrHash, args TransactionArgs) (acl types.AccessList, gasUsed uint64, vmErr error, err error) {
func AccessList(ctx context.Context, b CeloBackend, blockNrOrHash rpc.BlockNumberOrHash, args TransactionArgs) (acl types.AccessList, gasUsed uint64, vmErr error, err error) {
// Retrieve the execution context
db, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if db == nil || err != nil {
@@ -1728,7 +1747,17 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
statedb := db.Copy()
// Set the accesslist to the last al
args.AccessList = &accessList
msg := args.ToMessage(header.BaseFee)
exchangeRates := emptyExchangeRates
if args.FeeCurrency != nil {
// Always use the header's parent here, since we want to create the list at the
// queried block, but want to use the exchange rates before (at the beginning of)
// the queried block
exchangeRates, err = b.GetExchangeRates(ctx, header.ParentHash)
if err != nil {
return nil, 0, nil, fmt.Errorf("get exchange rates for block: %v err: %w", header.Hash(), err)
}
}
msg := args.ToMessage(header.BaseFee, exchangeRates)

// Apply the transaction with the access list tracer
tracer := logger.NewAccessListTracer(accessList, args.from(), to, precompiles)
@@ -1747,13 +1776,13 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH

// TransactionAPI exposes methods for reading and creating transaction data.
type TransactionAPI struct {
b Backend
b CeloBackend
nonceLock *AddrLocker
signer types.Signer
}

// NewTransactionAPI creates a new RPC service with methods for interacting with transactions.
func NewTransactionAPI(b Backend, nonceLock *AddrLocker) *TransactionAPI {
func NewTransactionAPI(b CeloBackend, nonceLock *AddrLocker) *TransactionAPI {
// The signer used by the API should always be the 'latest' known one because we expect
// signers to be backwards-compatible with old transactions.
signer := types.LatestSigner(b.ChainConfig())
52 changes: 49 additions & 3 deletions internal/ethapi/api_test.go
Original file line number Diff line number Diff line change
@@ -584,6 +584,50 @@ func newTestAccountManager(t *testing.T) (*accounts.Manager, accounts.Account) {
return am, acc
}

var errCeloNotImplemented error = errors.New("Celo backend test functionality not implemented")

type celoTestBackend struct {
*testBackend
}

func (c *celoTestBackend) GetFeeBalance(ctx context.Context, atBlock common.Hash, account common.Address, feeCurrency *common.Address) (*big.Int, error) {
if feeCurrency == nil {
header, err := c.HeaderByHash(ctx, atBlock)
if err != nil {
return nil, fmt.Errorf("retrieve header by hash in testBackend: %w", err)
}

state, _, err := c.StateAndHeaderByNumber(ctx, rpc.BlockNumber(header.Number.Int64()))
if err != nil {
return nil, err
}
return state.GetBalance(account).ToBig(), nil
}
// Celo specific backend features are currently not tested
return nil, errCeloNotImplemented
}

func (c *celoTestBackend) GetExchangeRates(ctx context.Context, atBlock common.Hash) (common.ExchangeRates, error) {
var er common.ExchangeRates
return er, nil
}

func (c *celoTestBackend) ConvertToCurrency(ctx context.Context, atBlock common.Hash, value *big.Int, feeCurrency *common.Address) (*big.Int, error) {
if feeCurrency == nil {
return value, nil
}
// Celo specific backend features are currently not tested
return nil, errCeloNotImplemented
}

func (c *celoTestBackend) ConvertToGold(ctx context.Context, atBlock common.Hash, value *big.Int, feeCurrency *common.Address) (*big.Int, error) {
if feeCurrency == nil {
return value, nil
}
// Celo specific backend features are currently not tested
return nil, errCeloNotImplemented
}

type testBackend struct {
db ethdb.Database
chain *core.BlockChain
@@ -592,7 +636,7 @@ type testBackend struct {
acc accounts.Account
}

func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.Engine, generator func(i int, b *core.BlockGen)) *testBackend {
func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.Engine, generator func(i int, b *core.BlockGen)) *celoTestBackend {
var (
cacheConfig = &core.CacheConfig{
TrieCleanLimit: 256,
@@ -616,7 +660,9 @@ func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.E
}

backend := &testBackend{db: db, chain: chain, accman: accman, acc: acc}
return backend
return &celoTestBackend{
testBackend: backend,
}
}

func (b *testBackend) setPendingBlock(block *types.Block) {
@@ -1990,7 +2036,7 @@ func TestRPCGetBlockOrHeader(t *testing.T) {
}
}

func setupReceiptBackend(t *testing.T, genBlocks int) (*testBackend, []common.Hash) {
func setupReceiptBackend(t *testing.T, genBlocks int) (*celoTestBackend, []common.Hash) {
config := *params.MergedTestChainConfig
var (
acc1Key, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
14 changes: 11 additions & 3 deletions internal/ethapi/backend.go
Original file line number Diff line number Diff line change
@@ -37,8 +37,16 @@ import (
"github.com/ethereum/go-ethereum/rpc"
)

// Backend interface provides the common API services (that are provided by
// both full and light clients) with access to necessary functions.
type CeloBackend interface {
Backend

GetFeeBalance(ctx context.Context, atBlock common.Hash, account common.Address, feeCurrency *common.Address) (*big.Int, error)
GetExchangeRates(ctx context.Context, atBlock common.Hash) (common.ExchangeRates, error)
ConvertToCurrency(ctx context.Context, atBlock common.Hash, value *big.Int, feeCurrency *common.Address) (*big.Int, error)
ConvertToGold(ctx context.Context, atBlock common.Hash, value *big.Int, feeCurrency *common.Address) (*big.Int, error)
}

// Backend interface provides the common API services (that are provided by both full and light clients) with access to necessary functions.
type Backend interface {
// General Ethereum API
SyncProgress() ethereum.SyncProgress
@@ -101,7 +109,7 @@ type Backend interface {
ServiceFilter(ctx context.Context, session *bloombits.MatcherSession)
}

func GetAPIs(apiBackend Backend) []rpc.API {
func GetAPIs(apiBackend CeloBackend) []rpc.API {
nonceLock := new(AddrLocker)
return []rpc.API{
{
67 changes: 61 additions & 6 deletions internal/ethapi/transaction_args.go
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ import (
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/exchange"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
@@ -74,6 +75,12 @@ type TransactionArgs struct {

// This configures whether blobs are allowed to be passed.
blobSidecarAllowed bool
// Celo specific

// CIP-64, CIP-66
FeeCurrency *common.Address `json:"feeCurrency,omitempty"`
// CIP-66
MaxFeeInFeeCurrency *hexutil.Big `json:"maxFeeInFeeCurrency,omitempty"`
}

// from retrieves the transaction sender address.
@@ -96,7 +103,7 @@ func (args *TransactionArgs) data() []byte {
}

// setDefaults fills in default values for unspecified tx fields.
func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, skipGasEstimation bool) error {
func (args *TransactionArgs) setDefaults(ctx context.Context, b CeloBackend, skipGasEstimation bool) error {
if err := args.setBlobTxSidecar(ctx); err != nil {
return err
}
@@ -158,6 +165,9 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, skipGas
AccessList: args.AccessList,
BlobFeeCap: args.BlobFeeCap,
BlobHashes: args.BlobHashes,

FeeCurrency: args.FeeCurrency,
MaxFeeInFeeCurrency: args.MaxFeeInFeeCurrency,
}
latestBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
estimated, err := DoEstimateGas(ctx, b, callArgs, latestBlockNr, nil, b.RPCGasCap())
@@ -183,7 +193,7 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, skipGas
}

// setFeeDefaults fills in default fee values for unspecified tx fields.
func (args *TransactionArgs) setFeeDefaults(ctx context.Context, b Backend) error {
func (args *TransactionArgs) setFeeDefaults(ctx context.Context, b CeloBackend) error {
head := b.CurrentHeader()
// Sanity check the EIP-4844 fee parameters.
if args.BlobFeeCap != nil && args.BlobFeeCap.ToInt().Sign() == 0 {
@@ -237,13 +247,19 @@ func (args *TransactionArgs) setFeeDefaults(ctx context.Context, b Backend) erro
if err != nil {
return err
}
if args.IsFeeCurrencyDenominated() {
price, err = b.ConvertToCurrency(ctx, head.Hash(), price, args.FeeCurrency)
if err != nil {
return fmt.Errorf("can't convert suggested gasTipCap to fee-currency: %w", err)
}
}
args.GasPrice = (*hexutil.Big)(price)
}
return nil
}

// setCancunFeeDefaults fills in reasonable default fee values for unspecified fields.
func (args *TransactionArgs) setCancunFeeDefaults(ctx context.Context, head *types.Header, b Backend) error {
func (args *TransactionArgs) setCancunFeeDefaults(ctx context.Context, head *types.Header, b CeloBackend) error {
// Set maxFeePerBlobGas if it is missing.
if args.BlobHashes != nil && args.BlobFeeCap == nil {
var excessBlobGas uint64
@@ -252,6 +268,15 @@ func (args *TransactionArgs) setCancunFeeDefaults(ctx context.Context, head *typ
}
// ExcessBlobGas must be set for a Cancun block.
blobBaseFee := eip4844.CalcBlobFee(excessBlobGas)
if args.IsFeeCurrencyDenominated() {
// wether the blob-fee will be used like that in Cel2 or not,
// at least this keeps it consistent with the rest of the gas-fees
var err error
blobBaseFee, err = b.ConvertToCurrency(ctx, head.Hash(), blobBaseFee, args.FeeCurrency)
if err != nil {
return fmt.Errorf("can't convert blob-fee to fee-currency: %w", err)
}
}
// Set the max fee to be 2 times larger than the previous block's blob base fee.
// The additional slack allows the tx to not become invalidated if the base
// fee is rising.
@@ -262,23 +287,37 @@ func (args *TransactionArgs) setCancunFeeDefaults(ctx context.Context, head *typ
}

// setLondonFeeDefaults fills in reasonable default fee values for unspecified fields.
func (args *TransactionArgs) setLondonFeeDefaults(ctx context.Context, head *types.Header, b Backend) error {
func (args *TransactionArgs) setLondonFeeDefaults(ctx context.Context, head *types.Header, b CeloBackend) error {
// Set maxPriorityFeePerGas if it is missing.
if args.MaxPriorityFeePerGas == nil {
tip, err := b.SuggestGasTipCap(ctx)
if err != nil {
return err
}
if args.IsFeeCurrencyDenominated() {
tip, err = b.ConvertToCurrency(ctx, head.Hash(), tip, args.FeeCurrency)
if err != nil {
return fmt.Errorf("can't convert suggested gasTipCap to fee-currency: %w", err)
}
}
args.MaxPriorityFeePerGas = (*hexutil.Big)(tip)
}
// Set maxFeePerGas if it is missing.
if args.MaxFeePerGas == nil {
// Set the max fee to be 2 times larger than the previous block's base fee.
// The additional slack allows the tx to not become invalidated if the base
// fee is rising.
baseFee := head.BaseFee
if args.IsFeeCurrencyDenominated() {
var err error
baseFee, err = b.ConvertToCurrency(ctx, head.Hash(), baseFee, args.FeeCurrency)
if err != nil {
return fmt.Errorf("can't convert base-fee to fee-currency: %w", err)
}
}
val := new(big.Int).Add(
args.MaxPriorityFeePerGas.ToInt(),
new(big.Int).Mul(head.BaseFee, big.NewInt(2)),
new(big.Int).Mul(baseFee, big.NewInt(2)),
)
args.MaxFeePerGas = (*hexutil.Big)(val)
}
@@ -421,7 +460,7 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int,
// core evm. This method is used in calls and traces that do not require a real
// live transaction.
// Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called.
func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message {
func (args *TransactionArgs) ToMessage(baseFee *big.Int, exchangeRates common.ExchangeRates) *core.Message {
var (
gasPrice *big.Int
gasFeeCap *big.Int
@@ -443,6 +482,14 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message {
// Backfill the legacy gasPrice for EVM execution, unless we're all zeroes
gasPrice = new(big.Int)
if gasFeeCap.BitLen() > 0 || gasTipCap.BitLen() > 0 {
if args.IsFeeCurrencyDenominated() {
var err error
baseFee, err = exchange.ConvertCeloToCurrency(exchangeRates, args.FeeCurrency, baseFee)
if err != nil {
log.Error("can't convert base-fee to fee-currency", "err", err)
baseFee = common.Big1
}
}
gasPrice = math.BigMin(new(big.Int).Add(gasTipCap, baseFee), gasFeeCap)
}
}
@@ -464,6 +511,7 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message {
BlobGasFeeCap: (*big.Int)(args.BlobFeeCap),
BlobHashes: args.BlobHashes,
SkipAccountChecks: true,
FeeCurrency: args.FeeCurrency,
}
}

@@ -544,3 +592,10 @@ func (args *TransactionArgs) ToTransaction() *types.Transaction {
func (args *TransactionArgs) IsEIP4844() bool {
return args.BlobHashes != nil || args.BlobFeeCap != nil
}

// IsFeeCurrencyDenominated returns whether the gas-price related
// fields are denominated in a given fee currency or in the native token.
// This effectively is only true for CIP-64 transactions.
func (args *TransactionArgs) IsFeeCurrencyDenominated() bool {
return args.FeeCurrency != nil && args.MaxFeeInFeeCurrency == nil
}
33 changes: 32 additions & 1 deletion internal/ethapi/transaction_args_test.go
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ func TestSetFeeDefaults(t *testing.T) {
}

var (
b = newBackendMock()
b = newCeloBackendMock()
zero = (*hexutil.Big)(big.NewInt(0))
fortytwo = (*hexutil.Big)(big.NewInt(42))
maxFee = (*hexutil.Big)(new(big.Int).Add(new(big.Int).Mul(b.current.BaseFee, big.NewInt(2)), fortytwo.ToInt()))
@@ -254,6 +254,37 @@ func TestSetFeeDefaults(t *testing.T) {
}
}

type celoBackendMock struct {
*backendMock
}

func newCeloBackendMock() *celoBackendMock {
return &celoBackendMock{
backendMock: newBackendMock(),
}
}

func (c *celoBackendMock) GetFeeBalance(ctx context.Context, atBlock common.Hash, account common.Address, feeCurrency *common.Address) (*big.Int, error) {
// Celo specific backend features are currently not tested
return nil, errCeloNotImplemented
}

func (c *celoBackendMock) GetExchangeRates(ctx context.Context, atBlock common.Hash) (common.ExchangeRates, error) {
var er common.ExchangeRates
// Celo specific backend features are currently not tested
return er, errCeloNotImplemented
}

func (c *celoBackendMock) ConvertToCurrency(ctx context.Context, atBlock common.Hash, value *big.Int, fromFeeCurrency *common.Address) (*big.Int, error) {
// Celo specific backend features are currently not tested
return nil, errCeloNotImplemented
}

func (c *celoBackendMock) ConvertToGold(ctx context.Context, atBlock common.Hash, value *big.Int, toFeeCurrency *common.Address) (*big.Int, error) {
// Celo specific backend features are currently not tested
return nil, errCeloNotImplemented
}

type backendMock struct {
current *types.Header
config *params.ChainConfig

0 comments on commit 03b5006

Please sign in to comment.