diff --git a/archive/client.go b/archive/client.go new file mode 100644 index 00000000..569f4ec1 --- /dev/null +++ b/archive/client.go @@ -0,0 +1,179 @@ +// 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, + heightMax uint64, +) (*Client, error) { + c, err := ethclient.DialContext(ctx, uri) + if err != nil { + return nil, fmt.Errorf("archive: failed to dial archival web3 node: %w", err) + } + + var latestBlock uint64 + switch heightMax { + case 0: + if latestBlock, err = c.BlockNumber(ctx); err != nil { + return nil, fmt.Errorf("archive: failed to query block number: %w", err) + } + default: + latestBlock = heightMax + } + + return &Client{ + inner: c, + latestBlock: latestBlock, + }, nil +} diff --git a/conf/config.go b/conf/config.go index c74f2301..46f61c7e 100644 --- a/conf/config.go +++ b/conf/config.go @@ -28,6 +28,15 @@ type Config struct { Cache *CacheConfig `koanf:"cache"` Database *DatabaseConfig `koanf:"database"` Gateway *GatewayConfig `koanf:"gateway"` + + // ArchiveURI is the URI of an archival web3 gateway instance + // for servicing historical queries. + ArchiveURI string `koanf:"archive_uri"` + // ArchiveHeightMax is the maximum height (inclusive) to query the + // archvie node (ArchiveURI). If the archive node is configured + // with it's own SQL database instance, this parameter should not + // be needed. + ArchiveHeightMax uint64 `koanf:"archive_height_max"` } // Validate performs config validation. diff --git a/main.go b/main.go index b1721fbd..4a957fe0 100644 --- a/main.go +++ b/main.go @@ -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" @@ -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, cfg.ArchiveHeightMax); 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{ diff --git a/rpc/apis.go b/rpc/apis.go index 0f5d88d7..05a1c6d0 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -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" @@ -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, @@ -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) diff --git a/rpc/eth/api.go b/rpc/eth/api.go index 0ec225dc..e4243952 100644 --- a/rpc/eth/api.go +++ b/rpc/eth/api.go @@ -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" @@ -108,6 +109,7 @@ type API interface { type publicAPI struct { client client.RuntimeClient + archiveClient *archive.Client backend indexer.Backend gasPriceOracle gas.Backend chainID uint32 @@ -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, @@ -126,6 +129,7 @@ func NewPublicAPI( ) API { return &publicAPI{ client: client, + archiveClient: archiveClient, chainID: chainID, Logger: logger, backend: backend, @@ -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 { @@ -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`. @@ -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) @@ -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) @@ -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) @@ -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{} @@ -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") } diff --git a/tests/rpc/utils.go b/tests/rpc/utils.go index aa386873..460536a7 100644 --- a/tests/rpc/utils.go +++ b/tests/rpc/utils.go @@ -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 {