@lodestar/prover 1.35.0-dev.e18102ed8c → 1.35.0-dev.f45a2be721
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lodestar-prover.js +3 -0
- package/lib/browser/index.d.ts.map +1 -0
- package/lib/cli/applyPreset.d.ts.map +1 -0
- package/lib/cli/cli.d.ts.map +1 -0
- package/lib/cli/cmds/index.d.ts.map +1 -0
- package/lib/cli/cmds/start/handler.d.ts.map +1 -0
- package/lib/cli/cmds/start/index.d.ts.map +1 -0
- package/lib/cli/cmds/start/options.d.ts.map +1 -0
- package/lib/cli/index.d.ts.map +1 -0
- package/lib/cli/options.d.ts.map +1 -0
- package/lib/constants.d.ts.map +1 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/interfaces.d.ts.map +1 -0
- package/lib/proof_provider/index.d.ts.map +1 -0
- package/lib/proof_provider/ordered_map.d.ts.map +1 -0
- package/lib/proof_provider/payload_store.d.ts.map +1 -0
- package/lib/proof_provider/proof_provider.d.ts.map +1 -0
- package/lib/provider_types/eip1193_provider_type.d.ts.map +1 -0
- package/lib/provider_types/ethers_provider_type.d.ts.map +1 -0
- package/lib/provider_types/legacy_provider_type.d.ts.map +1 -0
- package/lib/provider_types/web3_js_provider_type.d.ts.map +1 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/utils/assertion.d.ts.map +1 -0
- package/lib/utils/consensus.d.ts.map +1 -0
- package/lib/utils/conversion.d.ts.map +1 -0
- package/lib/utils/errors.d.ts.map +1 -0
- package/lib/utils/evm.d.ts.map +1 -0
- package/lib/utils/execution.d.ts.map +1 -0
- package/lib/utils/file.d.ts.map +1 -0
- package/lib/utils/gitData/gitDataPath.d.ts.map +1 -0
- package/lib/utils/gitData/index.d.ts.map +1 -0
- package/lib/utils/gitData/writeGitData.d.ts.map +1 -0
- package/lib/utils/json_rpc.d.ts.map +1 -0
- package/lib/utils/process.d.ts.map +1 -0
- package/lib/utils/req_resp.d.ts.map +1 -0
- package/lib/utils/rpc_provider.d.ts.map +1 -0
- package/lib/utils/validation.d.ts.map +1 -0
- package/lib/utils/verification.d.ts.map +1 -0
- package/lib/utils/version.d.ts.map +1 -0
- package/lib/verified_requests/eth_call.d.ts.map +1 -0
- package/lib/verified_requests/eth_estimateGas.d.ts.map +1 -0
- package/lib/verified_requests/eth_getBalance.d.ts.map +1 -0
- package/lib/verified_requests/eth_getBlockByHash.d.ts.map +1 -0
- package/lib/verified_requests/eth_getBlockByNumber.d.ts.map +1 -0
- package/lib/verified_requests/eth_getCode.d.ts.map +1 -0
- package/lib/verified_requests/eth_getTransactionCount.d.ts.map +1 -0
- package/lib/web3_provider.d.ts.map +1 -0
- package/lib/web3_provider_inspector.d.ts.map +1 -0
- package/lib/web3_proxy.d.ts.map +1 -0
- package/package.json +16 -15
- package/src/browser/index.ts +3 -0
- package/src/cli/applyPreset.ts +83 -0
- package/src/cli/cli.ts +58 -0
- package/src/cli/cmds/index.ts +7 -0
- package/src/cli/cmds/start/handler.ts +27 -0
- package/src/cli/cmds/start/index.ts +18 -0
- package/src/cli/cmds/start/options.ts +85 -0
- package/src/cli/index.ts +30 -0
- package/src/cli/options.ts +73 -0
- package/src/constants.ts +6 -0
- package/src/index.ts +5 -0
- package/src/interfaces.ts +90 -0
- package/src/proof_provider/index.ts +1 -0
- package/src/proof_provider/ordered_map.ts +25 -0
- package/src/proof_provider/payload_store.ts +223 -0
- package/src/proof_provider/proof_provider.ts +210 -0
- package/src/provider_types/eip1193_provider_type.ts +32 -0
- package/src/provider_types/ethers_provider_type.ts +44 -0
- package/src/provider_types/legacy_provider_type.ts +123 -0
- package/src/provider_types/web3_js_provider_type.ts +35 -0
- package/src/types.ts +163 -0
- package/src/utils/assertion.ts +11 -0
- package/src/utils/consensus.ts +122 -0
- package/src/utils/conversion.ts +107 -0
- package/src/utils/errors.ts +4 -0
- package/src/utils/evm.ts +284 -0
- package/src/utils/execution.ts +76 -0
- package/src/utils/file.ts +51 -0
- package/src/utils/gitData/gitDataPath.ts +48 -0
- package/src/utils/gitData/index.ts +70 -0
- package/src/utils/gitData/writeGitData.ts +10 -0
- package/src/utils/json_rpc.ts +170 -0
- package/src/utils/process.ts +111 -0
- package/src/utils/req_resp.ts +34 -0
- package/src/utils/rpc_provider.ts +117 -0
- package/src/utils/validation.ts +161 -0
- package/src/utils/verification.ts +112 -0
- package/src/utils/version.ts +74 -0
- package/src/verified_requests/eth_call.ts +50 -0
- package/src/verified_requests/eth_estimateGas.ts +49 -0
- package/src/verified_requests/eth_getBalance.ts +26 -0
- package/src/verified_requests/eth_getBlockByHash.ts +24 -0
- package/src/verified_requests/eth_getBlockByNumber.ts +25 -0
- package/src/verified_requests/eth_getCode.ts +50 -0
- package/src/verified_requests/eth_getTransactionCount.ts +26 -0
- package/src/web3_provider.ts +58 -0
- package/src/web3_provider_inspector.ts +88 -0
- package/src/web3_proxy.ts +175 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// MUST import first to apply preset from args and set ssz hasher
|
|
4
|
+
import "./applyPreset.js";
|
|
5
|
+
|
|
6
|
+
import {YargsError} from "../utils/errors.js";
|
|
7
|
+
import {getLodestarProverCli, yarg} from "./cli.js";
|
|
8
|
+
import "source-map-support/register.js";
|
|
9
|
+
|
|
10
|
+
const prover = getLodestarProverCli();
|
|
11
|
+
|
|
12
|
+
void prover
|
|
13
|
+
.fail((msg, err) => {
|
|
14
|
+
if (msg?.includes("Not enough non-option arguments")) {
|
|
15
|
+
// Show command help message when no command is provided
|
|
16
|
+
yarg.showHelp();
|
|
17
|
+
// biome-ignore lint/suspicious/noConsole: This code will run only in browser so console will be available.
|
|
18
|
+
console.log("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const errorMessage =
|
|
22
|
+
err !== undefined ? (err instanceof YargsError ? err.message : err.stack) : msg || "Unknown error";
|
|
23
|
+
|
|
24
|
+
// biome-ignore lint/suspicious/noConsole: We want to explicitly want to log error to console
|
|
25
|
+
console.error(` ✖ ${errorMessage}\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Execute CLI
|
|
30
|
+
.parse();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
|
|
2
|
+
import {ACTIVE_PRESET} from "@lodestar/params";
|
|
3
|
+
import {CliCommandOptions, LogLevel, LogLevels} from "@lodestar/utils";
|
|
4
|
+
import {YargsError} from "../utils/errors.js";
|
|
5
|
+
|
|
6
|
+
export type GlobalArgs = {
|
|
7
|
+
network?: string;
|
|
8
|
+
logLevel: string;
|
|
9
|
+
presetFile?: string;
|
|
10
|
+
preset: string;
|
|
11
|
+
paramsFile?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type GlobalOptions = {
|
|
15
|
+
logLevel: LogLevel;
|
|
16
|
+
} & ({paramsFile: string; network?: never} | {network: NetworkName; paramsFile?: never});
|
|
17
|
+
|
|
18
|
+
export const globalOptions: CliCommandOptions<GlobalArgs> = {
|
|
19
|
+
network: {
|
|
20
|
+
description: "Specify the network to connect.",
|
|
21
|
+
type: "string",
|
|
22
|
+
choices: [
|
|
23
|
+
...Object.keys(networksChainConfig), // Leave always as last network. The order matters for the --help printout
|
|
24
|
+
"dev",
|
|
25
|
+
],
|
|
26
|
+
conflicts: ["paramsFile"],
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
paramsFile: {
|
|
30
|
+
description: "Network configuration file",
|
|
31
|
+
type: "string",
|
|
32
|
+
conflicts: ["network"],
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
logLevel: {
|
|
36
|
+
description: "Set the log level.",
|
|
37
|
+
type: "string",
|
|
38
|
+
choices: LogLevels,
|
|
39
|
+
default: "info",
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// hidden option to allow for LODESTAR_PRESET to be set
|
|
43
|
+
preset: {
|
|
44
|
+
hidden: true,
|
|
45
|
+
type: "string",
|
|
46
|
+
default: ACTIVE_PRESET,
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
presetFile: {
|
|
50
|
+
hidden: true,
|
|
51
|
+
description: "Preset configuration file to override the active preset with custom values",
|
|
52
|
+
type: "string",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function parseGlobalArgs(args: GlobalArgs): GlobalOptions {
|
|
57
|
+
// Remove undefined values to allow deepmerge to inject default values downstream
|
|
58
|
+
if (args.network) {
|
|
59
|
+
return {
|
|
60
|
+
network: args.network as NetworkName,
|
|
61
|
+
logLevel: args.logLevel as LogLevel,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (args.paramsFile) {
|
|
66
|
+
return {
|
|
67
|
+
logLevel: args.logLevel as LogLevel,
|
|
68
|
+
paramsFile: args.paramsFile,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new YargsError("Either --network or --paramsFile must be provided");
|
|
73
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#configuration
|
|
2
|
+
export const MAX_REQUEST_LIGHT_CLIENT_UPDATES = 128;
|
|
3
|
+
export const MAX_PAYLOAD_HISTORY = 32;
|
|
4
|
+
export const VERIFICATION_FAILED_RESPONSE_CODE = -33091;
|
|
5
|
+
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
6
|
+
export const DEFAULT_PROXY_REQUEST_TIMEOUT = 3000;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from "./interfaces.js";
|
|
2
|
+
export * from "./proof_provider/index.js";
|
|
3
|
+
export {isVerificationFailedError} from "./utils/json_rpc.js";
|
|
4
|
+
export {createVerifiedExecutionProvider} from "./web3_provider.js";
|
|
5
|
+
export {createVerifiedExecutionProxy} from "./web3_proxy.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {ChainConfig} from "@lodestar/config";
|
|
2
|
+
import {NetworkName} from "@lodestar/config/networks";
|
|
3
|
+
import {LogLevel, Logger} from "@lodestar/utils";
|
|
4
|
+
import {ProofProvider} from "./proof_provider/proof_provider.js";
|
|
5
|
+
import {JsonRpcRequest, JsonRpcRequestOrBatch, JsonRpcResponse, JsonRpcResponseOrBatch} from "./types.js";
|
|
6
|
+
import {ELRpcProvider} from "./utils/rpc_provider.js";
|
|
7
|
+
|
|
8
|
+
export type {NetworkName} from "@lodestar/config/networks";
|
|
9
|
+
export enum LCTransport {
|
|
10
|
+
Rest = "Rest",
|
|
11
|
+
P2P = "P2P",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Provide either network or config. This will be helpful to connect to a custom network
|
|
15
|
+
export type NetworkOrConfig = {network: NetworkName; config?: never} | {network?: never; config: Partial<ChainConfig>};
|
|
16
|
+
|
|
17
|
+
export type RootProviderInitOptions = ConsensusNodeOptions &
|
|
18
|
+
NetworkOrConfig & {
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
logger: Logger;
|
|
21
|
+
wsCheckpoint?: string;
|
|
22
|
+
unverifiedWhitelist?: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// The `undefined` is necessary to match the types for the web3 1.x
|
|
26
|
+
export type ELRequestHandler<Params = unknown[], Response = unknown> = (
|
|
27
|
+
payload: JsonRpcRequestOrBatch<Params>
|
|
28
|
+
) => Promise<JsonRpcResponseOrBatch<Response> | undefined>;
|
|
29
|
+
|
|
30
|
+
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
|
|
31
|
+
export type ELRequestHandlerAny = ELRequestHandler<any, any>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated Kept for backward compatibility. Use `AnyWeb3Provider` type instead.
|
|
35
|
+
*/
|
|
36
|
+
export type Web3Provider = object;
|
|
37
|
+
|
|
38
|
+
export type ELVerifiedRequestHandlerOpts<Params = unknown[]> = {
|
|
39
|
+
payload: JsonRpcRequest<Params>;
|
|
40
|
+
rpc: ELRpcProvider;
|
|
41
|
+
proofProvider: ProofProvider;
|
|
42
|
+
logger: Logger;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ELVerifiedRequestHandler<Params = unknown[], Response = unknown> = (
|
|
46
|
+
opts: ELVerifiedRequestHandlerOpts<Params>
|
|
47
|
+
) => Promise<JsonRpcResponse<Response>>;
|
|
48
|
+
|
|
49
|
+
// Either a logger is provided by user or user specify a log level
|
|
50
|
+
// If both are skipped then we don't log anything (useful for browser plugins)
|
|
51
|
+
export type LogOptions = {logger?: Logger; logLevel?: never} | {logLevel?: LogLevel; logger?: never};
|
|
52
|
+
|
|
53
|
+
export type ConsensusNodeOptions =
|
|
54
|
+
| {transport: LCTransport.Rest; urls: string[]}
|
|
55
|
+
| {transport: LCTransport.P2P; bootnodes: string[]};
|
|
56
|
+
|
|
57
|
+
export type RootProviderOptions = {
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
wsCheckpoint?: string;
|
|
60
|
+
unverifiedWhitelist?: string[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type ProviderTypeOptions<T extends boolean | undefined> = {
|
|
64
|
+
/**
|
|
65
|
+
* If user specify custom provider types we will register those at the start in given order.
|
|
66
|
+
* So if you provider [custom1, custom2] and we already have [web3js, ethers] then final order
|
|
67
|
+
* of providers will be [custom1, custom2, web3js, ethers]
|
|
68
|
+
*/
|
|
69
|
+
providerTypes?: Web3ProviderType<AnyWeb3Provider>[];
|
|
70
|
+
/**
|
|
71
|
+
* To keep the backward compatible behavior if this option is not set we consider `true` as default.
|
|
72
|
+
* In coming breaking release we may set this option default to `false`.
|
|
73
|
+
*/
|
|
74
|
+
mutateProvider?: T;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type VerifiedExecutionInitOptions<T extends boolean | undefined> = LogOptions &
|
|
78
|
+
ConsensusNodeOptions &
|
|
79
|
+
NetworkOrConfig &
|
|
80
|
+
RootProviderOptions &
|
|
81
|
+
ProviderTypeOptions<T>;
|
|
82
|
+
|
|
83
|
+
export type AnyWeb3Provider = object;
|
|
84
|
+
|
|
85
|
+
export interface Web3ProviderType<T extends AnyWeb3Provider> {
|
|
86
|
+
name: string;
|
|
87
|
+
matched: (provider: AnyWeb3Provider) => provider is T;
|
|
88
|
+
handler(provider: T): ELRpcProvider["handler"];
|
|
89
|
+
mutateProvider(provider: T, newHandler: ELRpcProvider["handler"]): void;
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./proof_provider.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class OrderedMap<T> extends Map<number, T> {
|
|
2
|
+
private _min?: number;
|
|
3
|
+
private _max?: number;
|
|
4
|
+
|
|
5
|
+
get min(): number | undefined {
|
|
6
|
+
return this._min;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get max(): number | undefined {
|
|
10
|
+
return this._max;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
set(key: number, value: T): this {
|
|
14
|
+
if (this._min === undefined || key < this._min) {
|
|
15
|
+
this._min = key;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (this._max === undefined || key > this._max) {
|
|
19
|
+
this._max = key;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
super.set(key, value);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {ApiClient} from "@lodestar/api";
|
|
2
|
+
import {ForkName} from "@lodestar/params";
|
|
3
|
+
import {ExecutionPayload, LightClientHeader} from "@lodestar/types";
|
|
4
|
+
import {Logger} from "@lodestar/utils";
|
|
5
|
+
import {MAX_PAYLOAD_HISTORY} from "../constants.js";
|
|
6
|
+
import {fetchBlock, getExecutionPayloadForBlockNumber} from "../utils/consensus.js";
|
|
7
|
+
import {bufferToHex, hexToNumber} from "../utils/conversion.js";
|
|
8
|
+
import {OrderedMap} from "./ordered_map.js";
|
|
9
|
+
|
|
10
|
+
type BlockELRoot = string;
|
|
11
|
+
type BlockELRootAndSlot = {
|
|
12
|
+
blockELRoot: BlockELRoot;
|
|
13
|
+
slot: number;
|
|
14
|
+
};
|
|
15
|
+
type BlockCLRoot = string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The in-memory store for the execution payloads to be used to verify the proofs
|
|
19
|
+
*/
|
|
20
|
+
export class PayloadStore {
|
|
21
|
+
// We store the block root from execution for finalized blocks
|
|
22
|
+
// As these blocks are finalized, so not to be worried about conflicting roots
|
|
23
|
+
private finalizedRoots = new OrderedMap<BlockELRootAndSlot>();
|
|
24
|
+
|
|
25
|
+
// Unfinalized blocks may change over time and may have conflicting roots
|
|
26
|
+
// We can receive multiple light-client headers for the same block of execution
|
|
27
|
+
// So we why store unfinalized payloads by their CL root, which is only used
|
|
28
|
+
// in processing the light-client headers
|
|
29
|
+
private unfinalizedRoots = new Map<BlockCLRoot, BlockELRoot>();
|
|
30
|
+
|
|
31
|
+
// Payloads store with BlockELRoot as key
|
|
32
|
+
private payloads = new Map<BlockELRoot, ExecutionPayload>();
|
|
33
|
+
|
|
34
|
+
private latestBlockRoot: BlockELRoot | null = null;
|
|
35
|
+
|
|
36
|
+
constructor(private opts: {api: ApiClient; logger: Logger}) {}
|
|
37
|
+
|
|
38
|
+
get finalized(): ExecutionPayload | undefined {
|
|
39
|
+
const maxBlockNumberForFinalized = this.finalizedRoots.max;
|
|
40
|
+
|
|
41
|
+
if (maxBlockNumberForFinalized === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const finalizedMaxRoot = this.finalizedRoots.get(maxBlockNumberForFinalized);
|
|
46
|
+
if (finalizedMaxRoot) {
|
|
47
|
+
return this.payloads.get(finalizedMaxRoot.blockELRoot);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get latest(): ExecutionPayload | undefined {
|
|
54
|
+
if (this.latestBlockRoot) {
|
|
55
|
+
return this.payloads.get(this.latestBlockRoot);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async get(blockId: number | string): Promise<ExecutionPayload | undefined> {
|
|
62
|
+
// Given block id is a block hash in hex (32 bytes root takes 64 hex chars + 2 for 0x prefix)
|
|
63
|
+
if (typeof blockId === "string" && blockId.startsWith("0x") && blockId.length === 64 + 2) {
|
|
64
|
+
return this.payloads.get(blockId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Given block id is a block number in hex
|
|
68
|
+
if (typeof blockId === "string" && blockId.startsWith("0x")) {
|
|
69
|
+
return this.getOrFetchFinalizedPayload(hexToNumber(blockId));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Given block id is a block number in decimal string
|
|
73
|
+
if (typeof blockId === "string" && !blockId.startsWith("0x")) {
|
|
74
|
+
return this.getOrFetchFinalizedPayload(parseInt(blockId, 10));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Given block id is a block number in decimal
|
|
78
|
+
if (typeof blockId === "number") {
|
|
79
|
+
return this.getOrFetchFinalizedPayload(blockId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected async getOrFetchFinalizedPayload(blockNumber: number): Promise<ExecutionPayload | undefined> {
|
|
86
|
+
const maxBlockNumberForFinalized = this.finalizedRoots.max;
|
|
87
|
+
const minBlockNumberForFinalized = this.finalizedRoots.min;
|
|
88
|
+
|
|
89
|
+
if (maxBlockNumberForFinalized === undefined || minBlockNumberForFinalized === undefined) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (blockNumber > maxBlockNumberForFinalized) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Block number ${blockNumber} is higher than the latest finalized block number. We recommend to use block hash for unfinalized blocks.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let blockELRoot = this.finalizedRoots.get(blockNumber);
|
|
100
|
+
// check if we have payload cached locally else fetch from api
|
|
101
|
+
if (!blockELRoot) {
|
|
102
|
+
const finalizedMaxRoot = this.finalizedRoots.get(maxBlockNumberForFinalized);
|
|
103
|
+
const slot = finalizedMaxRoot?.slot;
|
|
104
|
+
if (slot !== undefined) {
|
|
105
|
+
const payloads = await getExecutionPayloadForBlockNumber(this.opts.api, slot, blockNumber);
|
|
106
|
+
for (const [slot, payload] of payloads.entries()) {
|
|
107
|
+
this.set(payload, slot, true);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
blockELRoot = this.finalizedRoots.get(blockNumber);
|
|
113
|
+
if (blockELRoot) {
|
|
114
|
+
return this.payloads.get(blockELRoot.blockELRoot);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
set(payload: ExecutionPayload, slot: number, finalized: boolean): void {
|
|
121
|
+
const blockELRoot = bufferToHex(payload.blockHash);
|
|
122
|
+
this.payloads.set(blockELRoot, payload);
|
|
123
|
+
|
|
124
|
+
if (this.latestBlockRoot) {
|
|
125
|
+
const latestPayload = this.payloads.get(this.latestBlockRoot);
|
|
126
|
+
if (latestPayload && latestPayload.blockNumber < payload.blockNumber) {
|
|
127
|
+
this.latestBlockRoot = blockELRoot;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
this.latestBlockRoot = blockELRoot;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (finalized) {
|
|
134
|
+
this.finalizedRoots.set(payload.blockNumber, {blockELRoot, slot});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async processLCHeader(header: LightClientHeader<ForkName.capella>, finalized = false): Promise<void> {
|
|
139
|
+
const blockSlot = header.beacon.slot;
|
|
140
|
+
const blockNumber = header.execution.blockNumber;
|
|
141
|
+
const blockELRoot = bufferToHex(header.execution.blockHash);
|
|
142
|
+
const blockCLRoot = bufferToHex(header.beacon.stateRoot);
|
|
143
|
+
const existingELRoot = this.unfinalizedRoots.get(blockCLRoot);
|
|
144
|
+
|
|
145
|
+
// ==== Finalized blocks ====
|
|
146
|
+
// if the block is finalized, we need to update the finalizedRoots map
|
|
147
|
+
if (finalized) {
|
|
148
|
+
this.finalizedRoots.set(blockNumber, {blockELRoot, slot: blockSlot});
|
|
149
|
+
|
|
150
|
+
// If the block is finalized and we already have the payload
|
|
151
|
+
// We can remove it from the unfinalizedRoots map and do nothing else
|
|
152
|
+
if (existingELRoot) {
|
|
153
|
+
this.unfinalizedRoots.delete(blockCLRoot);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If the block is finalized and we do not have the payload
|
|
157
|
+
// We need to fetch and set the payload
|
|
158
|
+
else {
|
|
159
|
+
const block = await fetchBlock(this.opts.api, blockSlot);
|
|
160
|
+
if (block) {
|
|
161
|
+
this.payloads.set(blockELRoot, block.message.body.executionPayload);
|
|
162
|
+
} else {
|
|
163
|
+
this.opts.logger.error("Failed to fetch block", blockSlot);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ==== Unfinalized blocks ====
|
|
171
|
+
// We already have the payload for this block
|
|
172
|
+
if (existingELRoot && existingELRoot === blockELRoot) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Re-org happened, we need to update the payload
|
|
177
|
+
if (existingELRoot && existingELRoot !== blockELRoot) {
|
|
178
|
+
this.payloads.delete(existingELRoot);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// This is unfinalized header we need to store it's root related to cl root
|
|
182
|
+
this.unfinalizedRoots.set(blockCLRoot, blockELRoot);
|
|
183
|
+
|
|
184
|
+
// We do not have the payload for this block, we need to fetch it
|
|
185
|
+
const block = await fetchBlock(this.opts.api, blockSlot);
|
|
186
|
+
if (block) {
|
|
187
|
+
this.set(block.message.body.executionPayload, blockSlot, false);
|
|
188
|
+
} else {
|
|
189
|
+
this.opts.logger.error("Failed to fetch finalized block", blockSlot);
|
|
190
|
+
}
|
|
191
|
+
this.prune();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
prune(): void {
|
|
195
|
+
if (this.finalizedRoots.size <= MAX_PAYLOAD_HISTORY) return;
|
|
196
|
+
// Store doe not have any finalized blocks means it's recently initialized
|
|
197
|
+
if (this.finalizedRoots.max === undefined || this.finalizedRoots.min === undefined) return;
|
|
198
|
+
|
|
199
|
+
for (
|
|
200
|
+
let blockNumber = this.finalizedRoots.max - MAX_PAYLOAD_HISTORY;
|
|
201
|
+
blockNumber >= this.finalizedRoots.min;
|
|
202
|
+
blockNumber--
|
|
203
|
+
) {
|
|
204
|
+
const blockELRoot = this.finalizedRoots.get(blockNumber);
|
|
205
|
+
if (blockELRoot) {
|
|
206
|
+
this.payloads.delete(blockELRoot.blockELRoot);
|
|
207
|
+
this.finalizedRoots.delete(blockNumber);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const [clRoot, elRoot] of this.unfinalizedRoots) {
|
|
212
|
+
const payload = this.payloads.get(elRoot);
|
|
213
|
+
if (!payload) {
|
|
214
|
+
this.unfinalizedRoots.delete(clRoot);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (payload.blockNumber < this.finalizedRoots.min) {
|
|
219
|
+
this.unfinalizedRoots.delete(clRoot);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {ApiClient, getClient} from "@lodestar/api/beacon";
|
|
2
|
+
import {ChainForkConfig, createChainForkConfig} from "@lodestar/config";
|
|
3
|
+
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
|
|
4
|
+
import {Lightclient, LightclientEvent, RunStatusCode} from "@lodestar/light-client";
|
|
5
|
+
import {LightClientRestTransport} from "@lodestar/light-client/transport";
|
|
6
|
+
import {ForkName, isForkPostCapella} from "@lodestar/params";
|
|
7
|
+
import {ExecutionPayload, LightClientHeader} from "@lodestar/types";
|
|
8
|
+
import {Logger} from "@lodestar/utils";
|
|
9
|
+
import {LCTransport, RootProviderInitOptions} from "../interfaces.js";
|
|
10
|
+
import {assertLightClient} from "../utils/assertion.js";
|
|
11
|
+
import {
|
|
12
|
+
fetchBlock,
|
|
13
|
+
getExecutionPayloads,
|
|
14
|
+
getGenesisData,
|
|
15
|
+
getSyncCheckpoint,
|
|
16
|
+
getUnFinalizedRangeForPayloads,
|
|
17
|
+
} from "../utils/consensus.js";
|
|
18
|
+
import {bufferToHex} from "../utils/conversion.js";
|
|
19
|
+
import {PayloadStore} from "./payload_store.js";
|
|
20
|
+
|
|
21
|
+
type RootProviderOptions = Omit<RootProviderInitOptions, "transport"> & {
|
|
22
|
+
transport: LightClientRestTransport;
|
|
23
|
+
api: ApiClient;
|
|
24
|
+
config: ChainForkConfig;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class ProofProvider {
|
|
28
|
+
private store: PayloadStore;
|
|
29
|
+
private logger: Logger;
|
|
30
|
+
// Make sure readyPromise doesn't throw unhandled exceptions
|
|
31
|
+
private readyPromise?: Promise<void>;
|
|
32
|
+
|
|
33
|
+
readonly config: ChainForkConfig;
|
|
34
|
+
readonly network: NetworkName;
|
|
35
|
+
readonly api: ApiClient;
|
|
36
|
+
|
|
37
|
+
lightClient?: Lightclient;
|
|
38
|
+
|
|
39
|
+
constructor(readonly opts: RootProviderOptions) {
|
|
40
|
+
this.store = new PayloadStore({api: opts.api, logger: opts.logger});
|
|
41
|
+
this.logger = opts.logger;
|
|
42
|
+
this.config = opts.config;
|
|
43
|
+
this.api = opts.api;
|
|
44
|
+
this.network = opts.config.PRESET_BASE as NetworkName;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async waitToBeReady(): Promise<void> {
|
|
48
|
+
return this.readyPromise;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static init(opts: RootProviderInitOptions): ProofProvider {
|
|
52
|
+
if (opts.transport === LCTransport.P2P) {
|
|
53
|
+
throw new Error("P2P mode not supported yet");
|
|
54
|
+
}
|
|
55
|
+
opts.logger.info("Creating ProofProvider instance with REST APIs", {
|
|
56
|
+
network: opts.network,
|
|
57
|
+
urls: opts.urls.join(","),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const config = opts.network
|
|
61
|
+
? createChainForkConfig(networksChainConfig[opts.network])
|
|
62
|
+
: createChainForkConfig(opts.config);
|
|
63
|
+
|
|
64
|
+
const api = getClient({urls: opts.urls}, {config});
|
|
65
|
+
const transport = new LightClientRestTransport(api);
|
|
66
|
+
|
|
67
|
+
const provider = new ProofProvider({
|
|
68
|
+
...opts,
|
|
69
|
+
config,
|
|
70
|
+
api,
|
|
71
|
+
transport,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
provider.readyPromise = provider.sync(opts.wsCheckpoint).catch((e) => {
|
|
75
|
+
opts.logger.error("Error while syncing", e);
|
|
76
|
+
return Promise.reject(e);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return provider;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async sync(wsCheckpoint?: string): Promise<void> {
|
|
83
|
+
if (this.lightClient !== undefined) {
|
|
84
|
+
throw Error("Light client already initialized and syncing.");
|
|
85
|
+
}
|
|
86
|
+
this.logger.info("Starting sync for proof provider");
|
|
87
|
+
const {api, config, transport} = this.opts;
|
|
88
|
+
const checkpointRoot = await getSyncCheckpoint(api, wsCheckpoint);
|
|
89
|
+
const genesisData = await getGenesisData(api);
|
|
90
|
+
|
|
91
|
+
this.logger.info("Initializing lightclient", {checkpointRoot: bufferToHex(checkpointRoot)});
|
|
92
|
+
this.lightClient = await Lightclient.initializeFromCheckpointRoot({
|
|
93
|
+
checkpointRoot,
|
|
94
|
+
config,
|
|
95
|
+
transport,
|
|
96
|
+
genesisData,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
assertLightClient(this.lightClient);
|
|
100
|
+
|
|
101
|
+
this.logger.info("Initiating lightclient");
|
|
102
|
+
// Wait for the lightclient to start
|
|
103
|
+
await this.lightClient?.start();
|
|
104
|
+
this.logger.info("Lightclient synced", this.getStatus());
|
|
105
|
+
this.registerEvents();
|
|
106
|
+
|
|
107
|
+
// Load the payloads from the CL
|
|
108
|
+
this.logger.info("Building EL payload history");
|
|
109
|
+
const {start, end} = await getUnFinalizedRangeForPayloads(this.lightClient);
|
|
110
|
+
const payloads = await getExecutionPayloads({
|
|
111
|
+
api: this.opts.api,
|
|
112
|
+
startSlot: start,
|
|
113
|
+
endSlot: end,
|
|
114
|
+
logger: this.logger,
|
|
115
|
+
});
|
|
116
|
+
for (const [slot, payload] of payloads.entries()) {
|
|
117
|
+
this.store.set(payload, slot, false);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Load the finalized payload from the CL
|
|
121
|
+
const finalizedSlot = this.lightClient.getFinalized().beacon.slot;
|
|
122
|
+
this.logger.debug("Getting finalized slot from lightclient", {finalizedSlot});
|
|
123
|
+
const block = await fetchBlock(this.opts.api, finalizedSlot);
|
|
124
|
+
if (block) {
|
|
125
|
+
this.store.set(block.message.body.executionPayload, finalizedSlot, true);
|
|
126
|
+
} else {
|
|
127
|
+
this.logger.error("Failed to fetch finalized block", finalizedSlot);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.logger.info("Proof provider ready");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getStatus(): {latest: number; finalized: number; status: RunStatusCode} {
|
|
134
|
+
if (!this.lightClient) {
|
|
135
|
+
return {
|
|
136
|
+
latest: 0,
|
|
137
|
+
finalized: 0,
|
|
138
|
+
status: RunStatusCode.uninitialized,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
latest: this.lightClient.getHead().beacon.slot,
|
|
144
|
+
finalized: this.lightClient.getFinalized().beacon.slot,
|
|
145
|
+
status: this.lightClient.status,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getExecutionPayload(blockNumber: number | string | "finalized" | "latest"): Promise<ExecutionPayload> {
|
|
150
|
+
assertLightClient(this.lightClient);
|
|
151
|
+
|
|
152
|
+
if (typeof blockNumber === "string" && blockNumber === "finalized") {
|
|
153
|
+
const payload = this.store.finalized;
|
|
154
|
+
if (!payload) throw new Error("No finalized payload");
|
|
155
|
+
return payload;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (typeof blockNumber === "string" && blockNumber === "latest") {
|
|
159
|
+
const payload = this.store.latest;
|
|
160
|
+
if (!payload) throw new Error("No latest payload");
|
|
161
|
+
return payload;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ((typeof blockNumber === "string" && blockNumber.startsWith("0x")) || typeof blockNumber === "number") {
|
|
165
|
+
const payload = await this.store.get(blockNumber);
|
|
166
|
+
if (!payload) throw new Error(`No payload for blockNumber ${blockNumber}`);
|
|
167
|
+
return payload;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new Error(`Invalid blockNumber "${blockNumber}"`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async processLCHeader(lcHeader: LightClientHeader, finalized = false): Promise<void> {
|
|
174
|
+
const fork = this.opts.config.getForkName(lcHeader.beacon.slot);
|
|
175
|
+
|
|
176
|
+
if (!isForkPostCapella(fork)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sszType = this.opts.config.getPostBellatrixForkTypes(lcHeader.beacon.slot).ExecutionPayloadHeader;
|
|
181
|
+
if (
|
|
182
|
+
isForkPostCapella(fork) &&
|
|
183
|
+
(!("execution" in lcHeader) || sszType.equals(lcHeader.execution, sszType.defaultValue()))
|
|
184
|
+
) {
|
|
185
|
+
throw new Error("Execution payload is required for execution fork");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await this.store.processLCHeader(lcHeader as LightClientHeader<ForkName.capella>, finalized);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private registerEvents(): void {
|
|
192
|
+
assertLightClient(this.lightClient);
|
|
193
|
+
|
|
194
|
+
this.opts.signal.addEventListener("abort", () => {
|
|
195
|
+
this.lightClient?.stop();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.lightClient.emitter.on(LightclientEvent.lightClientFinalityHeader, async (data) => {
|
|
199
|
+
await this.processLCHeader(data, true).catch((e) => {
|
|
200
|
+
this.logger.error("Error processing finality update", null, e);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.lightClient.emitter.on(LightclientEvent.lightClientOptimisticHeader, async (data) => {
|
|
205
|
+
await this.processLCHeader(data).catch((e) => {
|
|
206
|
+
this.logger.error("Error processing optimistic update", null, e);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|