Skip to content

Commit

Permalink
Add Backend wrapper for Celo functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
ezdac committed May 14, 2024
1 parent c5ac47e commit 8252069
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 37 deletions.
5 changes: 3 additions & 2 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,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())...)
Expand Down Expand Up @@ -391,7 +392,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),
},
}...)
}
Expand Down
2 changes: 1 addition & 1 deletion graphql/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 3 additions & 1 deletion graphql/graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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"

Expand Down Expand Up @@ -477,7 +478,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)
}
Expand Down
6 changes: 4 additions & 2 deletions graphql/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/celoapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type CeloAPI struct {
eth Ethereum
}

func NewCeloAPI(e Ethereum, b ethapi.Backend) *CeloAPI {
func NewCeloAPI(e Ethereum, b ethapi.CeloBackend) *CeloAPI {
return &CeloAPI{
ethAPI: ethapi.NewEthereumAPI(b),
eth: e,
Expand Down
101 changes: 101 additions & 0 deletions internal/celoapi/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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/lru"
"github.com/ethereum/go-ethereum/contracts"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/rpc"
)

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

// 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

exchangeRatesCache *lru.Cache[common.Hash, common.ExchangeRates]
}

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 &contracts.CeloBackend{
ChainConfig: b.Backend.ChainConfig(),
State: state,
}, nil
}

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, err
}
return contracts.GetFeeBalance(cb, account, feeCurrency), nil
}

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)
}
b.exchangeRatesCache.Add(atBlock, er)
return er, nil
}

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
}
currencyValue, err := exchange.ConvertGoldToCurrency(er, fromFeeCurrency, value)
if err != nil {
return nil, fmt.Errorf("could not convert to native from fee currency (%s): %w ", fromFeeCurrency, err)
}
return currencyValue, nil
}

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
}
goldValue, err := exchange.ConvertCurrencyToGold(er, value, toFeeCurrency)
if err != nil {
return nil, fmt.Errorf("could not convert from native to fee currency (%s): %w ", toFeeCurrency, err)
}
return goldValue, nil
}
18 changes: 9 additions & 9 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,11 @@ func (s *EthereumAccountAPI) Accounts() []common.Address {
type PersonalAccountAPI struct {
am *accounts.Manager
nonceLock *AddrLocker
b Backend
b CeloBackend
}

// NewPersonalAccountAPI create a new PersonalAccountAPI.
func NewPersonalAccountAPI(b Backend, nonceLock *AddrLocker) *PersonalAccountAPI {
func NewPersonalAccountAPI(b CeloBackend, nonceLock *AddrLocker) *PersonalAccountAPI {
return &PersonalAccountAPI{
am: b.AccountManager(),
nonceLock: nonceLock,
Expand Down Expand Up @@ -606,11 +606,11 @@ func (s *PersonalAccountAPI) Unpair(ctx context.Context, url string, pin string)

// 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}
}

Expand Down Expand Up @@ -1195,7 +1195,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)
Expand Down Expand Up @@ -1296,7 +1296,7 @@ func executeEstimate(ctx context.Context, b Backend, args TransactionArgs, state
// 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) {
// 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
Expand Down Expand Up @@ -1781,7 +1781,7 @@ func (s *BlockChainAPI) CreateAccessList(ctx context.Context, args TransactionAr
// 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 {
Expand Down Expand Up @@ -1843,13 +1843,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())
Expand Down
68 changes: 56 additions & 12 deletions internal/ethapi/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,22 +562,64 @@ func allBlobTxs(addr common.Address, config *params.ChainConfig) []txData {
}
}

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), 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
pending *types.Block
}

func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.Engine, generator func(i int, b *core.BlockGen)) *testBackend {
var (
cacheConfig = &core.CacheConfig{
TrieCleanLimit: 256,
TrieDirtyLimit: 256,
TrieTimeLimit: 5 * time.Minute,
SnapshotLimit: 0,
TrieDirtyDisabled: true, // Archive mode
}
)
func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.Engine, generator func(i int, b *core.BlockGen)) *celoTestBackend {
cacheConfig := &core.CacheConfig{
TrieCleanLimit: 256,
TrieDirtyLimit: 256,
TrieTimeLimit: 5 * time.Minute,
SnapshotLimit: 0,
TrieDirtyDisabled: true, // Archive mode
}
// Generate blocks for testing
db, blocks, _ := core.GenerateChainWithGenesis(gspec, engine, n, generator)
txlookupLimit := uint64(0)
Expand All @@ -590,7 +632,9 @@ func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.E
}

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

func (b *testBackend) setPendingBlock(block *types.Block) {
Expand Down Expand Up @@ -1568,7 +1612,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.TestChainConfigNoCel2
config.ShanghaiTime = new(uint64)
config.CancunTime = new(uint64)
Expand Down
14 changes: 11 additions & 3 deletions internal/ethapi/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
{
Expand Down
Loading

0 comments on commit 8252069

Please sign in to comment.