UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

143 lines 6.81 kB
import { createElapsedTimeTracker, fromHex, isErrorAborted, isFetchError, toHex, toPrintableUrl, } from "@lodestar/utils"; import { HTTP_CONNECTION_ERROR_CODES, HTTP_FATAL_ERROR_CODES } from "../../execution/engine/utils.js"; import { isValidAddress } from "../../util/address.js"; import { linspace } from "../../util/numpy.js"; import { Eth1ProviderState } from "../interface.js"; import { DEFAULT_PROVIDER_URLS } from "../options.js"; import { depositEventTopics, parseDepositLog } from "../utils/depositContract.js"; import { ErrorJsonRpcResponse, HttpRpcError, JsonRpcHttpClient, JsonRpcHttpClientEvent, } from "./jsonRpcHttpClient.js"; import { dataToBytes, isJsonRpcTruncatedError, numToQuantity, quantityToNum } from "./utils.js"; // Define static options once to prevent extra allocations const getBlocksByNumberOpts = { routeId: "getBlockByNumber_batched" }; const getBlockByNumberOpts = { routeId: "getBlockByNumber" }; const getBlockByHashOpts = { routeId: "getBlockByHash" }; const getBlockNumberOpts = { routeId: "getBlockNumber" }; const getLogsOpts = { routeId: "getLogs" }; const isOneMinutePassed = createElapsedTimeTracker({ minElapsedTime: 60_000 }); export class Eth1Provider { constructor(config, opts, signal, metrics) { // The default state is ONLINE, it will be updated to offline if we receive a http error this.state = Eth1ProviderState.ONLINE; this.logger = opts.logger; this.deployBlock = opts.depositContractDeployBlock ?? 0; this.depositContractAddress = toHex(config.DEPOSIT_CONTRACT_ADDRESS); const providerUrls = opts.providerUrls ?? DEFAULT_PROVIDER_URLS; this.rpc = new JsonRpcHttpClient(providerUrls, { signal, // Don't fallback with is truncated error. Throw early and let the retry on this class handle it shouldNotFallback: isJsonRpcTruncatedError, jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined, jwtId: opts.jwtId, jwtVersion: opts.jwtVersion, metrics: metrics, }); this.logger?.info("Eth1 provider", { urls: providerUrls.map(toPrintableUrl).toString() }); this.rpc.emitter.on(JsonRpcHttpClientEvent.RESPONSE, () => { const oldState = this.state; this.state = Eth1ProviderState.ONLINE; if (oldState !== Eth1ProviderState.ONLINE) { this.logger?.info("Eth1 provider is back online", { oldState, newState: this.state }); } }); this.rpc.emitter.on(JsonRpcHttpClientEvent.ERROR, ({ error }) => { if (isErrorAborted(error)) { this.state = Eth1ProviderState.ONLINE; } else if (error instanceof HttpRpcError || error instanceof ErrorJsonRpcResponse) { this.state = Eth1ProviderState.ERROR; } else if (error && isFetchError(error) && HTTP_FATAL_ERROR_CODES.includes(error.code)) { this.state = Eth1ProviderState.OFFLINE; } else if (error && isFetchError(error) && HTTP_CONNECTION_ERROR_CODES.includes(error.code)) { this.state = Eth1ProviderState.AUTH_FAILED; } if (this.state !== Eth1ProviderState.ONLINE && isOneMinutePassed()) { this.logger?.error("Eth1 provider error", { state: this.state, lastErrorAt: new Date(Date.now() - isOneMinutePassed.msSinceLastCall).toLocaleTimeString(), }, error); } }); } getState() { return this.state; } async validateContract() { if (!isValidAddress(this.depositContractAddress)) { throw Error(`Invalid contract address: ${this.depositContractAddress}`); } const code = await this.getCode(this.depositContractAddress); if (!code || code === "0x") { throw new Error(`There is no deposit contract at given address: ${this.depositContractAddress}`); } } async getDepositEvents(fromBlock, toBlock) { const logsRawArr = await this.getLogs({ fromBlock, toBlock, address: this.depositContractAddress, topics: depositEventTopics, }); return logsRawArr.flat(1).map((log) => parseDepositLog(log)); } /** * Fetches an arbitrary array of block numbers in batch */ async getBlocksByNumber(fromBlock, toBlock) { const method = "eth_getBlockByNumber"; const blocksArr = await this.rpc.fetchBatch(linspace(fromBlock, toBlock).map((blockNumber) => ({ method, params: [numToQuantity(blockNumber), false] })), getBlocksByNumberOpts); const blocks = []; for (const block of blocksArr.flat(1)) { if (block) blocks.push(block); } return blocks; } async getBlockByNumber(blockNumber) { const method = "eth_getBlockByNumber"; const blockNumberHex = typeof blockNumber === "string" ? blockNumber : numToQuantity(blockNumber); return this.rpc.fetch( // false = include only transaction roots, not full objects { method, params: [blockNumberHex, false] }, getBlockByNumberOpts); } async getBlockByHash(blockHashHex) { const method = "eth_getBlockByHash"; return this.rpc.fetch( // false = include only transaction roots, not full objects { method, params: [blockHashHex, false] }, getBlockByHashOpts); } async getBlockNumber() { const method = "eth_blockNumber"; const blockNumberRaw = await this.rpc.fetch({ method, params: [] }, getBlockNumberOpts); return parseInt(blockNumberRaw, 16); } async getCode(address) { const method = "eth_getCode"; return this.rpc.fetch({ method, params: [address, "latest"] }); } async getLogs(options) { const method = "eth_getLogs"; const hexOptions = { ...options, fromBlock: numToQuantity(options.fromBlock), toBlock: numToQuantity(options.toBlock), }; const logsRaw = await this.rpc.fetch({ method, params: [hexOptions] }, getLogsOpts); return logsRaw.map((logRaw) => ({ blockNumber: parseInt(logRaw.blockNumber, 16), data: logRaw.data, topics: logRaw.topics, })); } } export function parseEth1Block(blockRaw) { if (typeof blockRaw !== "object") throw Error("block is not an object"); return { blockHash: dataToBytes(blockRaw.hash, 32), blockNumber: quantityToNum(blockRaw.number, "block.number"), timestamp: quantityToNum(blockRaw.timestamp, "block.timestamp"), }; } //# sourceMappingURL=eth1Provider.js.map