Skip to content

Commit

Permalink
feat: add archive web3 gw support
Browse files Browse the repository at this point in the history
  • Loading branch information
Yawning committed May 27, 2022
1 parent bbf6e77 commit c8c2df1
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 13 deletions.
173 changes: 173 additions & 0 deletions archive/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Package archive implements an archive node client.
package archive

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

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/ethclient"

"github.com/oasisprotocol/emerald-web3-gateway/rpc/utils"
)

// Client is an archive node client backed by web, implementing a limited
// subset of rpc/eth.API, that is sufficient to support historical queries.
//
// All of the parameters that are `ethrpc.BlockNumberOrHash` just assume
// that the caller will handle converting to a block number, because they
// need to anyway, and historical estimate gas calls are not supported.
type Client struct {
inner *ethclient.Client
latestBlock uint64
}

func (c *Client) LatestBlock() uint64 {
return c.latestBlock
}

func (c *Client) GetStorageAt(
ctx context.Context,
address common.Address,
position hexutil.Big,
blockNr uint64,
) (hexutil.Big, error) {
storageBytes, err := c.inner.StorageAt(
ctx,
address,
common.BigToHash((*big.Int)(&position)),
new(big.Int).SetUint64(blockNr),
)
if err != nil {
return hexutil.Big{}, fmt.Errorf("archive: failed to query storage: %w", err)
}

// Oh for fuck's sake.
var storageBig big.Int
storageBig.SetBytes(storageBytes)
return hexutil.Big(storageBig), nil
}

func (c *Client) GetBalance(
ctx context.Context,
address common.Address,
blockNr uint64,
) (*hexutil.Big, error) {
balance, err := c.inner.BalanceAt(
ctx,
address,
new(big.Int).SetUint64(blockNr),
)
if err != nil {
return nil, fmt.Errorf("archive: failed to query balance: %w", err)
}

return (*hexutil.Big)(balance), nil
}

func (c *Client) GetTransactionCount(
ctx context.Context,
address common.Address,
blockNr uint64,
) (*hexutil.Uint64, error) {
nonce, err := c.inner.NonceAt(
ctx,
address,
new(big.Int).SetUint64(blockNr),
)
if err != nil {
return nil, fmt.Errorf("archive: failed to query nonce: %w", err)
}

return (*hexutil.Uint64)(&nonce), nil
}

func (c *Client) GetCode(
ctx context.Context,
address common.Address,
blockNr uint64,
) (hexutil.Bytes, error) {
code, err := c.inner.CodeAt(
ctx,
address,
new(big.Int).SetUint64(blockNr),
)
if err != nil {
return nil, fmt.Errorf("archive: failed to query code: %w", err)
}

return hexutil.Bytes(code), nil
}

func (c *Client) Call(
ctx context.Context,
args utils.TransactionArgs,
blockNr uint64,
) (hexutil.Bytes, error) {
// You have got to be fucking shitting me, what in the actual fuck.

if args.From == nil {
return nil, fmt.Errorf("archive: no `from` in call")
}
callMsg := ethereum.CallMsg{
From: *args.From,
To: args.To,
GasPrice: (*big.Int)(args.GasPrice),
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
Value: (*big.Int)(args.Value),
// args.Nonce? I guess it can't be that important if there's no field for it.
}
if args.Gas != nil {
callMsg.Gas = uint64(*args.Gas)
}
if args.Data != nil {
callMsg.Data = []byte(*args.Data)
}
if args.Input != nil {
// Data and Input are the same damn thing, Input is newer.
callMsg.Data = []byte(*args.Input)
}
if args.AccessList != nil {
callMsg.AccessList = *args.AccessList
}

result, err := c.inner.CallContract(
ctx,
callMsg,
new(big.Int).SetUint64(blockNr),
)
if err != nil {
return nil, fmt.Errorf("archive: failed to call contract: %w", err)
}

return hexutil.Bytes(result), nil
}

func (c *Client) Close() {
c.inner.Close()
c.inner = nil
}

func New(
ctx context.Context,
uri string,
) (*Client, error) {
c, err := ethclient.DialContext(ctx, uri)
if err != nil {
return nil, fmt.Errorf("archive: failed to dial archival web3 node: %w", err)
}

latestBlock, err := c.BlockNumber(ctx)
if err != nil {
return nil, fmt.Errorf("archive: failed to query block number: %w", err)
}

return &Client{
inner: c,
latestBlock: latestBlock,
}, nil
}
2 changes: 2 additions & 0 deletions conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type Config struct {
Cache *CacheConfig `koanf:"cache"`
Database *DatabaseConfig `koanf:"database"`
Gateway *GatewayConfig `koanf:"gateway"`

ArchiveURI string `kaonf:"archive_uri"`
}

// Validate performs config validation.
Expand Down
11 changes: 10 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

"github.com/oasisprotocol/emerald-web3-gateway/archive"
"github.com/oasisprotocol/emerald-web3-gateway/conf"
"github.com/oasisprotocol/emerald-web3-gateway/db/migrations"
"github.com/oasisprotocol/emerald-web3-gateway/filters"
Expand Down Expand Up @@ -245,7 +246,15 @@ func runRoot() error {
return err
}

w3.RegisterAPIs(rpc.GetRPCAPIs(ctx, rc, backend, gasPriceOracle, cfg.Gateway, es))
var archiveClient *archive.Client
if cfg.ArchiveURI != "" {
if archiveClient, err = archive.New(ctx, cfg.ArchiveURI); err != nil {
logger.Error("failed to create archive client", err)
return err
}
}

w3.RegisterAPIs(rpc.GetRPCAPIs(ctx, rc, archiveClient, backend, gasPriceOracle, cfg.Gateway, es))
w3.RegisterHealthChecks([]server.HealthCheck{indx})

svr := server.Server{
Expand Down
4 changes: 3 additions & 1 deletion rpc/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/oasisprotocol/oasis-core/go/common/logging"
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/client"

"github.com/oasisprotocol/emerald-web3-gateway/archive"
"github.com/oasisprotocol/emerald-web3-gateway/conf"
eventFilters "github.com/oasisprotocol/emerald-web3-gateway/filters"
"github.com/oasisprotocol/emerald-web3-gateway/gas"
Expand All @@ -23,6 +24,7 @@ import (
func GetRPCAPIs(
ctx context.Context,
client client.RuntimeClient,
archiveClient *archive.Client,
backend indexer.Backend,
gasPriceOracle gas.Backend,
config *conf.GatewayConfig,
Expand All @@ -31,7 +33,7 @@ func GetRPCAPIs(
var apis []ethRpc.API

web3Service := web3.NewPublicAPI()
ethService := eth.NewPublicAPI(client, logging.GetLogger("eth_rpc"), config.ChainID, backend, gasPriceOracle, config.MethodLimits)
ethService := eth.NewPublicAPI(client, archiveClient, logging.GetLogger("eth_rpc"), config.ChainID, backend, gasPriceOracle, config.MethodLimits)
netService := net.NewPublicAPI(config.ChainID)
txpoolService := txpool.NewPublicAPI()
filtersService := filters.NewPublicAPI(client, logging.GetLogger("eth_filters"), backend, eventSystem)
Expand Down
53 changes: 43 additions & 10 deletions rpc/eth/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/evm"
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/types"

"github.com/oasisprotocol/emerald-web3-gateway/archive"
"github.com/oasisprotocol/emerald-web3-gateway/conf"
"github.com/oasisprotocol/emerald-web3-gateway/gas"
"github.com/oasisprotocol/emerald-web3-gateway/indexer"
Expand Down Expand Up @@ -108,6 +109,7 @@ type API interface {

type publicAPI struct {
client client.RuntimeClient
archiveClient *archive.Client
backend indexer.Backend
gasPriceOracle gas.Backend
chainID uint32
Expand All @@ -118,6 +120,7 @@ type publicAPI struct {
// NewPublicAPI creates an instance of the public ETH Web3 API.
func NewPublicAPI(
client client.RuntimeClient,
archiveClient *archive.Client,
logger *logging.Logger,
chainID uint32,
backend indexer.Backend,
Expand All @@ -126,6 +129,7 @@ func NewPublicAPI(
) API {
return &publicAPI{
client: client,
archiveClient: archiveClient,
chainID: chainID,
Logger: logger,
backend: backend,
Expand All @@ -149,6 +153,15 @@ func handleStorageError(logger *logging.Logger, err error) error {
return ErrInternalError
}

func (api *publicAPI) shouldQueryArchive(n uint64) bool {
// If there is no archive node configured, return false.
if api.archiveClient == nil {
return false
}

return n <= api.archiveClient.LatestBlock()
}

// roundParamFromBlockNum converts special BlockNumber values to the corresponding special round numbers.
func (api *publicAPI) roundParamFromBlockNum(ctx context.Context, logger *logging.Logger, blockNum ethrpc.BlockNumber) (uint64, error) {
switch blockNum {
Expand Down Expand Up @@ -226,6 +239,10 @@ func (api *publicAPI) GetStorageAt(ctx context.Context, address common.Address,
if err != nil {
return hexutil.Big{}, err
}
if api.shouldQueryArchive(round) {
return api.archiveClient.GetStorageAt(ctx, address, position, round)
}

// EVM module takes index as H256, which needs leading zeros.
position256 := make([]byte, 32)
// Unmarshalling to hexutil.Big rejects overlong inputs. Verify in `TestRejectOverlong`.
Expand All @@ -247,11 +264,15 @@ func (api *publicAPI) GetBalance(ctx context.Context, address common.Address, bl
logger := api.Logger.With("method", "eth_getBalance", "address", address, "block_or_hash", blockNrOrHash)
logger.Debug("request")

ethmod := evm.NewV1(api.client)
round, err := api.getBlockRound(ctx, logger, blockNrOrHash)
if err != nil {
return nil, err
}
if api.shouldQueryArchive(round) {
return api.archiveClient.GetBalance(ctx, address, round)
}

ethmod := evm.NewV1(api.client)
res, err := ethmod.Balance(ctx, round, address[:])
if err != nil {
logger.Error("ethmod.Balance failed", "round", round, "err", err)
Expand Down Expand Up @@ -291,13 +312,17 @@ func (api *publicAPI) GetTransactionCount(ctx context.Context, ethAddr common.Ad
logger := api.Logger.With("method", "eth_getBlockTransactionCount", "address", ethAddr, "block_or_hash", blockNrOrHash)
logger.Debug("request")

accountsMod := accounts.NewV1(api.client)
accountsAddr := types.NewAddressRaw(types.AddressV0Secp256k1EthContext, ethAddr[:])

round, err := api.getBlockRound(ctx, logger, blockNrOrHash)
if err != nil {
return nil, err
}
if api.shouldQueryArchive(round) {
return api.archiveClient.GetTransactionCount(ctx, ethAddr, round)
}

accountsMod := accounts.NewV1(api.client)
accountsAddr := types.NewAddressRaw(types.AddressV0Secp256k1EthContext, ethAddr[:])

nonce, err := accountsMod.Nonce(ctx, round, accountsAddr)
if err != nil {
logger.Error("accounts.Nonce failed", "err", err)
Expand All @@ -311,11 +336,15 @@ func (api *publicAPI) GetCode(ctx context.Context, address common.Address, block
logger := api.Logger.With("method", "eth_getCode", "address", address, "block_or_hash", blockNrOrHash)
logger.Debug("request")

ethmod := evm.NewV1(api.client)
round, err := api.getBlockRound(ctx, logger, blockNrOrHash)
if err != nil {
return nil, err
}
if api.shouldQueryArchive(round) {
return api.archiveClient.GetCode(ctx, address, round)
}

ethmod := evm.NewV1(api.client)
res, err := ethmod.Code(ctx, round, address[:])
if err != nil {
logger.Error("ethmod.Code failed", "err", err)
Expand Down Expand Up @@ -361,6 +390,15 @@ func (api *publicAPI) NewRevertError(revertErr error) *RevertError {
func (api *publicAPI) Call(ctx context.Context, args utils.TransactionArgs, blockNrOrHash ethrpc.BlockNumberOrHash, _ *utils.StateOverride) (hexutil.Bytes, error) {
logger := api.Logger.With("method", "eth_call", "block_or_hash", blockNrOrHash)
logger.Debug("request", "args", args)

round, err := api.getBlockRound(ctx, logger, blockNrOrHash)
if err != nil {
return nil, err
}
if api.shouldQueryArchive(round) {
return api.archiveClient.Call(ctx, args, round)
}

var (
amount = []byte{0}
input = []byte{}
Expand All @@ -370,11 +408,6 @@ func (api *publicAPI) Call(ctx context.Context, args utils.TransactionArgs, bloc
gas uint64 = 30_000_000
)

round, err := api.getBlockRound(ctx, logger, blockNrOrHash)
if err != nil {
return nil, err
}

if args.To == nil {
return []byte{}, errors.New("to address not specified")
}
Expand Down
2 changes: 1 addition & 1 deletion tests/rpc/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func Setup() error {
return fmt.Errorf("setup: failed starting gas price oracle: %w", err)
}

w3.RegisterAPIs(rpc.GetRPCAPIs(context.Background(), rc, backend, gasPriceOracle, tests.TestsConfig.Gateway, es))
w3.RegisterAPIs(rpc.GetRPCAPIs(context.Background(), rc, nil, backend, gasPriceOracle, tests.TestsConfig.Gateway, es))
w3.RegisterHealthChecks([]server.HealthCheck{indx})

if err = w3.Start(); err != nil {
Expand Down

0 comments on commit c8c2df1

Please sign in to comment.