diff --git a/arbitrum/apibackend.go b/arbitrum/apibackend.go index d78addcb8..bd496062f 100644 --- a/arbitrum/apibackend.go +++ b/arbitrum/apibackend.go @@ -48,8 +48,9 @@ type APIBackend struct { dbForAPICalls ethdb.Database - fallbackClient types.FallbackClient - sync SyncProgressBackend + fallbackClient types.FallbackClient + archiveClientsManager *archiveFallbackClientsManager + sync SyncProgressBackend } type errorFilteredFallbackClient struct { @@ -79,11 +80,11 @@ func (c *timeoutFallbackClient) CallContext(ctxIn context.Context, result interf return c.impl.CallContext(ctx, result, method, args...) } -func CreateFallbackClient(fallbackClientUrl string, fallbackClientTimeout time.Duration) (types.FallbackClient, error) { +func CreateFallbackClient(fallbackClientUrl string, fallbackClientTimeout time.Duration, isArchiveNode bool) (types.FallbackClient, error) { if fallbackClientUrl == "" { return nil, nil } - if strings.HasPrefix(fallbackClientUrl, "error:") { + if !isArchiveNode && strings.HasPrefix(fallbackClientUrl, "error:") { fields := strings.Split(fallbackClientUrl, ":")[1:] errNumber, convErr := strconv.ParseInt(fields[0], 0, 0) if convErr == nil { @@ -124,8 +125,8 @@ type SyncProgressBackend interface { BlockMetadataByNumber(ctx context.Context, blockNum uint64) (common.BlockMetadata, error) } -func createRegisterAPIBackend(backend *Backend, filterConfig filters.Config, fallbackClientUrl string, fallbackClientTimeout time.Duration) (*filters.FilterSystem, error) { - fallbackClient, err := CreateFallbackClient(fallbackClientUrl, fallbackClientTimeout) +func createRegisterAPIBackend(backend *Backend, filterConfig filters.Config, fallbackClientUrl string, fallbackClientTimeout time.Duration, archiveRedirects []BlockRedirectConfig) (*filters.FilterSystem, error) { + fallbackClient, err := CreateFallbackClient(fallbackClientUrl, fallbackClientTimeout, false) if err != nil { return nil, err } @@ -135,10 +136,18 @@ func createRegisterAPIBackend(backend *Backend, filterConfig filters.Config, fal if tag != 0 || len(backend.chainDb.WasmTargets()) > 1 { dbForAPICalls = rawdb.WrapDatabaseWithWasm(backend.chainDb, wasmStore, 0, []ethdb.WasmTarget{rawdb.LocalTarget()}) } + var archiveClientsManager *archiveFallbackClientsManager + if len(archiveRedirects) != 0 { + archiveClientsManager, err = newArchiveFallbackClientsManager(archiveRedirects) + if err != nil { + return nil, err + } + } backend.apiBackend = &APIBackend{ - b: backend, - dbForAPICalls: dbForAPICalls, - fallbackClient: fallbackClient, + b: backend, + dbForAPICalls: dbForAPICalls, + fallbackClient: fallbackClient, + archiveClientsManager: archiveClientsManager, } filterSystem := filters.NewFilterSystem(backend.apiBackend, filterConfig) backend.stack.RegisterAPIs(backend.apiBackend.GetAPIs(filterSystem)) @@ -500,7 +509,7 @@ func (a *APIBackend) BlockMetadataByNumber(ctx context.Context, blockNum uint64) return a.sync.BlockMetadataByNumber(ctx, blockNum) } -func StateAndHeaderFromHeader(ctx context.Context, chainDb ethdb.Database, bc *core.BlockChain, maxRecreateStateDepth int64, header *types.Header, err error) (*state.StateDB, *types.Header, error) { +func StateAndHeaderFromHeader(ctx context.Context, chainDb ethdb.Database, bc *core.BlockChain, maxRecreateStateDepth int64, header *types.Header, err error, archiveClientsManager *archiveFallbackClientsManager) (*state.StateDB, *types.Header, error) { if err != nil { return nil, header, err } @@ -510,6 +519,9 @@ func StateAndHeaderFromHeader(ctx context.Context, chainDb ethdb.Database, bc *c if !bc.Config().IsArbitrumNitro(header.Number) { return nil, header, types.ErrUseFallback } + if archiveClientsManager != nil && header.Number.Uint64() <= archiveClientsManager.lastAvailableBlock() { + return nil, header, &types.ErrUseArchiveFallback{BlockNum: header.Number.Uint64()} + } stateFor := func(db state.Database, snapshots *snapshot.Tree) func(header *types.Header) (*state.StateDB, StateReleaseFunc, error) { return func(header *types.Header) (*state.StateDB, StateReleaseFunc, error) { if header.Root != (common.Hash{}) { @@ -583,7 +595,7 @@ func StateAndHeaderFromHeader(ctx context.Context, chainDb ethdb.Database, bc *c func (a *APIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) { header, err := a.HeaderByNumber(ctx, number) - return StateAndHeaderFromHeader(ctx, a.ChainDb(), a.b.arb.BlockChain(), a.b.config.MaxRecreateStateDepth, header, err) + return StateAndHeaderFromHeader(ctx, a.ChainDb(), a.b.arb.BlockChain(), a.b.config.MaxRecreateStateDepth, header, err, a.archiveClientsManager) } func (a *APIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { @@ -594,7 +606,7 @@ func (a *APIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOr if ishash && header != nil && header.Number.Cmp(bc.CurrentBlock().Number) > 0 && bc.GetCanonicalHash(header.Number.Uint64()) != hash { return nil, nil, errors.New("requested block ahead of current block and the hash is not currently canonical") } - return StateAndHeaderFromHeader(ctx, a.ChainDb(), a.b.arb.BlockChain(), a.b.config.MaxRecreateStateDepth, header, err) + return StateAndHeaderFromHeader(ctx, a.ChainDb(), a.b.arb.BlockChain(), a.b.config.MaxRecreateStateDepth, header, err, a.archiveClientsManager) } func (a *APIBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) { @@ -730,3 +742,7 @@ func (a *APIBackend) Pending() (*types.Block, types.Receipts, *state.StateDB) { func (a *APIBackend) FallbackClient() types.FallbackClient { return a.fallbackClient } + +func (a *APIBackend) ArchiveFallbackClient(blockNum uint64) types.FallbackClient { + return a.archiveClientsManager.fallbackClient(blockNum) +} diff --git a/arbitrum/archivefallbackclients.go b/arbitrum/archivefallbackclients.go new file mode 100644 index 000000000..bd893622e --- /dev/null +++ b/arbitrum/archivefallbackclients.go @@ -0,0 +1,66 @@ +package arbitrum + +import ( + "math/rand" + "sort" + + "github.com/ethereum/go-ethereum/core/types" +) + +type lastBlockAndClient struct { + lastBlock uint64 + client types.FallbackClient +} + +type archiveFallbackClientsManager struct { + lastBlockAndClients []*lastBlockAndClient +} + +func newArchiveFallbackClientsManager(archiveRedirects []BlockRedirectConfig) (*archiveFallbackClientsManager, error) { + manager := &archiveFallbackClientsManager{} + for _, archiveConfig := range archiveRedirects { + fallbackClient, err := CreateFallbackClient(archiveConfig.URL, archiveConfig.Timeout, true) + if err != nil { + return nil, err + } + if fallbackClient == nil { + continue + } + manager.lastBlockAndClients = append(manager.lastBlockAndClients, &lastBlockAndClient{ + lastBlock: archiveConfig.LastBlock, + client: fallbackClient, + }) + } + if len(manager.lastBlockAndClients) == 0 { + return nil, nil + } + sort.Slice(manager.lastBlockAndClients, func(i, j int) bool { + return manager.lastBlockAndClients[i].lastBlock < manager.lastBlockAndClients[j].lastBlock + }) + return manager, nil +} + +func (a *archiveFallbackClientsManager) lastAvailableBlock() uint64 { + return a.lastBlockAndClients[len(a.lastBlockAndClients)-1].lastBlock +} + +func (a *archiveFallbackClientsManager) fallbackClient(blockNum uint64) types.FallbackClient { + var possibleClients []types.FallbackClient + var chosenLastBlock uint64 + for _, lastBlockAndClient := range a.lastBlockAndClients { + if chosenLastBlock == 0 && blockNum <= lastBlockAndClient.lastBlock { + chosenLastBlock = lastBlockAndClient.lastBlock + } + if chosenLastBlock != 0 { + if lastBlockAndClient.lastBlock == chosenLastBlock { + possibleClients = append(possibleClients, lastBlockAndClient.client) + } else { + break + } + } + } + if len(possibleClients) != 0 { + return possibleClients[rand.Intn(len(possibleClients))] + } + return nil +} diff --git a/arbitrum/backend.go b/arbitrum/backend.go index e3f23ef71..b8e195d5a 100644 --- a/arbitrum/backend.go +++ b/arbitrum/backend.go @@ -65,7 +65,7 @@ func NewBackend(stack *node.Node, config *Config, chainDb ethdb.Database, publis } backend.bloomIndexer.Start(backend.arb.BlockChain()) - filterSystem, err := createRegisterAPIBackend(backend, filterConfig, config.ClassicRedirect, config.ClassicRedirectTimeout) + filterSystem, err := createRegisterAPIBackend(backend, filterConfig, config.ClassicRedirect, config.ClassicRedirectTimeout, config.BlockRedirects) if err != nil { return nil, nil, err } diff --git a/arbitrum/config.go b/arbitrum/config.go index 308cf9aee..c7fcb9f34 100644 --- a/arbitrum/config.go +++ b/arbitrum/config.go @@ -1,6 +1,8 @@ package arbitrum import ( + "encoding/json" + "fmt" "time" "github.com/ethereum/go-ethereum/eth/ethconfig" @@ -39,6 +41,27 @@ type Config struct { MaxRecreateStateDepth int64 `koanf:"max-recreate-state-depth"` AllowMethod []string `koanf:"allow-method"` + + BlockRedirects []BlockRedirectConfig `koanf:"block-redirects"` + BlockRedirectsList string `koanf:"block-redirects-list"` +} + +type BlockRedirectConfig struct { + URL string `koanf:"url"` + Timeout time.Duration `koanf:"timeout"` + LastBlock uint64 `koanf:"last-block"` +} + +func (c *Config) Validate() error { + // BlockRedirectsList command line option overrides directly supplied BlockRedirects array of BlockRedirectConfig in the conf file + if c.BlockRedirectsList != "default" { + var blockRedirects []BlockRedirectConfig + if err := json.Unmarshal([]byte(c.BlockRedirectsList), &blockRedirects); err != nil { + return fmt.Errorf("failed to parse rpc block-redirects-list string: %w", err) + } + c.BlockRedirects = blockRedirects + } + return nil } type ArbDebugConfig struct { @@ -63,6 +86,7 @@ func ConfigAddOptions(prefix string, f *flag.FlagSet) { arbDebug := DefaultConfig.ArbDebug f.Uint64(prefix+".arbdebug.block-range-bound", arbDebug.BlockRangeBound, "bounds the number of blocks arbdebug calls may return") f.Uint64(prefix+".arbdebug.timeout-queue-bound", arbDebug.TimeoutQueueBound, "bounds the length of timeout queues arbdebug calls may return") + f.String(prefix+".block-redirects-list", DefaultConfig.BlockRedirectsList, "array of node configs to redirect block requests given as a json string. time duration should be supplied in number indicating nanoseconds") } const ( @@ -89,4 +113,5 @@ var DefaultConfig = Config{ BlockRangeBound: 256, TimeoutQueueBound: 512, }, + BlockRedirectsList: "default", } diff --git a/core/types/arb_types.go b/core/types/arb_types.go index eb7609354..678b1c5cd 100644 --- a/core/types/arb_types.go +++ b/core/types/arb_types.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/binary" + "fmt" "math/big" "github.com/ethereum/go-ethereum/common/hexutil" @@ -41,6 +42,14 @@ func (f fallbackError) Error() string { return fallbackErrorMsg } var ErrUseFallback = fallbackError{} +type ErrUseArchiveFallback struct { + BlockNum uint64 +} + +func (e *ErrUseArchiveFallback) Error() string { + return fmt.Sprintf("use archive fallback client for block %d", e.BlockNum) +} + type FallbackClient interface { CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error } diff --git a/eth/api_backend.go b/eth/api_backend.go index 620446f66..2ab5f6859 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -441,3 +441,7 @@ func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Blo func (b *EthAPIBackend) FallbackClient() types.FallbackClient { return nil } + +func (b *EthAPIBackend) ArchiveFallbackClient(_ uint64) types.FallbackClient { + return nil +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 51f59800a..23fbbe629 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -57,10 +57,14 @@ var ( ) func fallbackClientFor(b Backend, err error) types.FallbackClient { - if !errors.Is(err, types.ErrUseFallback) { - return nil + if errors.Is(err, types.ErrUseFallback) { + return b.FallbackClient() + } + var archiveErr *types.ErrUseArchiveFallback + if errors.As(err, &archiveErr) { + return b.ArchiveFallbackClient(archiveErr.BlockNum) } - return b.FallbackClient() + return nil } // EthereumAPI provides an API to access Ethereum related information. diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 8559b161b..43e8d333a 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -628,6 +628,10 @@ func (b testBackend) FallbackClient() types.FallbackClient { return nil } +func (b testBackend) ArchiveFallbackClient(_ uint64) types.FallbackClient { + return nil +} + func (b testBackend) SyncProgressMap(ctx context.Context) map[string]interface{} { return map[string]interface{}{} } diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index af2ee5832..1db102104 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -41,6 +41,7 @@ import ( // both full and light clients) with access to necessary functions. type Backend interface { FallbackClient() types.FallbackClient + ArchiveFallbackClient(blockNum uint64) types.FallbackClient // General Ethereum API SyncProgress() ethereum.SyncProgress diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 7ce941599..0c419b459 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -410,6 +410,10 @@ func (b *backendMock) FallbackClient() types.FallbackClient { return nil } +func (b *backendMock) ArchiveFallbackClient(_ uint64) types.FallbackClient { + return nil +} + func (b *backendMock) SyncProgressMap(ctx context.Context) map[string]interface{} { return nil }