@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
143 lines • 6.81 kB
JavaScript
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