@lodestar/prover 1.35.0-dev.f80d2d52da → 1.35.0-dev.fd1dac853d
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 +3 -3
- package/lib/cli/cli.d.ts.map +1 -0
- package/lib/cli/cli.js +1 -1
- package/lib/cli/cli.js.map +1 -1
- package/lib/cli/cmds/index.d.ts.map +1 -0
- package/lib/cli/cmds/index.js.map +1 -1
- 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/index.js.map +1 -1
- package/lib/cli/options.d.ts.map +1 -0
- package/lib/constants.d.ts.map +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- 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/ordered_map.js +2 -0
- package/lib/proof_provider/ordered_map.js.map +1 -1
- package/lib/proof_provider/payload_store.d.ts.map +1 -0
- package/lib/proof_provider/payload_store.js +12 -11
- package/lib/proof_provider/payload_store.js.map +1 -1
- package/lib/proof_provider/proof_provider.d.ts.map +1 -0
- package/lib/proof_provider/proof_provider.js +9 -0
- package/lib/proof_provider/proof_provider.js.map +1 -1
- 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/file.js.map +1 -1
- package/lib/utils/gitData/gitDataPath.d.ts.map +1 -0
- package/lib/utils/gitData/index.d.ts.map +1 -0
- package/lib/utils/gitData/index.js.map +1 -1
- 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/rpc_provider.js +3 -1
- package/lib/utils/rpc_provider.js.map +1 -1
- package/lib/utils/validation.d.ts.map +1 -0
- package/lib/utils/validation.js.map +1 -1
- 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_provider_inspector.js +2 -1
- package/lib/web3_provider_inspector.js.map +1 -1
- package/lib/web3_proxy.d.ts.map +1 -0
- package/lib/web3_proxy.js +1 -1
- package/lib/web3_proxy.js.map +1 -1
- 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/types.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export type JsonRpcId = number | string;
|
|
2
|
+
export type JsonRpcVersion = string & ("2.0" | "1.0");
|
|
3
|
+
|
|
4
|
+
export interface JsonRpcRequestPayload<T = unknown[]> {
|
|
5
|
+
readonly jsonrpc: JsonRpcVersion;
|
|
6
|
+
readonly id: JsonRpcId;
|
|
7
|
+
readonly method: string;
|
|
8
|
+
readonly params: T;
|
|
9
|
+
readonly requestOptions?: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface JsonRpcNotificationPayload<T = unknown[]> {
|
|
13
|
+
readonly jsonrpc: JsonRpcVersion;
|
|
14
|
+
readonly method: string;
|
|
15
|
+
readonly params: T;
|
|
16
|
+
readonly requestOptions?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type JsonRpcRequest<T = unknown[]> = JsonRpcRequestPayload<T> | JsonRpcNotificationPayload<T>;
|
|
20
|
+
export type JsonRpcBatchRequest<T = unknown[]> = JsonRpcRequest<T>[];
|
|
21
|
+
|
|
22
|
+
// The request can be a single request, a notification
|
|
23
|
+
// or an array of requests and notifications as batch request
|
|
24
|
+
export type JsonRpcRequestOrBatch<T = unknown[]> = JsonRpcRequest<T> | JsonRpcBatchRequest<T>;
|
|
25
|
+
|
|
26
|
+
// Make the response compatible with different libraries, we don't use the readonly modifier
|
|
27
|
+
export interface JsonRpcResponseWithResultPayload<T> {
|
|
28
|
+
readonly id?: JsonRpcId;
|
|
29
|
+
jsonrpc: JsonRpcVersion;
|
|
30
|
+
result: T;
|
|
31
|
+
error?: never;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface JsonRpcErrorPayload<T> {
|
|
35
|
+
readonly code?: number;
|
|
36
|
+
readonly data?: T;
|
|
37
|
+
readonly message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface JsonRpcResponseWithErrorPayload<T> {
|
|
41
|
+
readonly id?: JsonRpcId;
|
|
42
|
+
jsonrpc: JsonRpcVersion;
|
|
43
|
+
result?: never;
|
|
44
|
+
error: JsonRpcErrorPayload<T>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Make the very flexible el response type to match different libraries easily
|
|
48
|
+
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
|
|
49
|
+
export type JsonRpcResponse<T = any, E = any> =
|
|
50
|
+
| JsonRpcResponseWithResultPayload<T>
|
|
51
|
+
| JsonRpcResponseWithErrorPayload<E>;
|
|
52
|
+
|
|
53
|
+
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
|
|
54
|
+
export type JsonRpcBatchResponse<T = any, E = any> = JsonRpcResponse<T, E>[];
|
|
55
|
+
|
|
56
|
+
// Response can be a single response or an array of responses in case of batch request
|
|
57
|
+
// Make the very flexible el response type to match different libraries easily
|
|
58
|
+
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
|
|
59
|
+
export type JsonRpcResponseOrBatch<T = any, E = any> = JsonRpcResponse<T, E> | JsonRpcBatchResponse<T, E>;
|
|
60
|
+
|
|
61
|
+
export type HexString = string;
|
|
62
|
+
|
|
63
|
+
export type ELBlockNumberOrTag = number | string | "latest" | "earliest" | "pending";
|
|
64
|
+
|
|
65
|
+
export interface ELProof {
|
|
66
|
+
readonly address: string;
|
|
67
|
+
readonly balance: string;
|
|
68
|
+
readonly codeHash: string;
|
|
69
|
+
readonly nonce: string;
|
|
70
|
+
readonly storageHash: string;
|
|
71
|
+
readonly accountProof: string[];
|
|
72
|
+
readonly storageProof: {
|
|
73
|
+
readonly key: string;
|
|
74
|
+
readonly value: string;
|
|
75
|
+
readonly proof: string[];
|
|
76
|
+
}[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ELTransaction {
|
|
80
|
+
readonly type: string;
|
|
81
|
+
readonly nonce: string;
|
|
82
|
+
readonly to: string | null;
|
|
83
|
+
readonly chainId?: string;
|
|
84
|
+
readonly input: string;
|
|
85
|
+
readonly value: string;
|
|
86
|
+
readonly gasPrice?: string;
|
|
87
|
+
readonly gas: string;
|
|
88
|
+
readonly maxFeePerGas?: string;
|
|
89
|
+
readonly maxPriorityFeePerGas?: string;
|
|
90
|
+
readonly blockHash: string;
|
|
91
|
+
readonly blockNumber: string;
|
|
92
|
+
readonly from: string;
|
|
93
|
+
readonly hash: string;
|
|
94
|
+
readonly r: string;
|
|
95
|
+
readonly s: string;
|
|
96
|
+
readonly v: string;
|
|
97
|
+
readonly transactionIndex: string;
|
|
98
|
+
readonly accessList?: {address: string; storageKeys: string[]}[];
|
|
99
|
+
readonly data?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ELWithdrawal {
|
|
103
|
+
readonly index: string;
|
|
104
|
+
readonly validatorIndex: string;
|
|
105
|
+
readonly address: string;
|
|
106
|
+
readonly amount: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ELBlock {
|
|
110
|
+
readonly parentHash: string;
|
|
111
|
+
readonly stateRoot: string;
|
|
112
|
+
readonly receiptsRoot: string;
|
|
113
|
+
readonly logsBloom: string;
|
|
114
|
+
readonly nonce: string;
|
|
115
|
+
readonly difficulty: string;
|
|
116
|
+
readonly totalDifficulty: string;
|
|
117
|
+
readonly number: string;
|
|
118
|
+
readonly gasLimit: string;
|
|
119
|
+
readonly gasUsed: string;
|
|
120
|
+
readonly timestamp: string;
|
|
121
|
+
readonly extraData?: Buffer | string;
|
|
122
|
+
readonly mixHash: string;
|
|
123
|
+
readonly hash: string;
|
|
124
|
+
readonly baseFeePerGas: string;
|
|
125
|
+
readonly miner: string;
|
|
126
|
+
readonly sha3Uncles: string;
|
|
127
|
+
readonly size: string;
|
|
128
|
+
readonly uncles: ELBlock[];
|
|
129
|
+
readonly transactions: ELTransaction[];
|
|
130
|
+
readonly transactionsRoot: string;
|
|
131
|
+
readonly withdrawals?: ELWithdrawal[];
|
|
132
|
+
readonly withdrawalsRoot?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ELAccessList {
|
|
136
|
+
readonly address: HexString;
|
|
137
|
+
readonly storageKeys: HexString[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface ELAccessListResponse {
|
|
141
|
+
readonly error: string;
|
|
142
|
+
readonly gasUsed: HexString;
|
|
143
|
+
readonly accessList: ELAccessList[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type ELStorageProof = Pick<ELProof, "storageHash" | "storageProof">;
|
|
147
|
+
|
|
148
|
+
export type ELApi = {
|
|
149
|
+
eth_getBalance: (address: string, block?: number | string) => string;
|
|
150
|
+
eth_createAccessList: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => ELAccessListResponse;
|
|
151
|
+
eth_call: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => HexString;
|
|
152
|
+
eth_estimateGas: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => HexString;
|
|
153
|
+
eth_getCode: (address: string, block?: ELBlockNumberOrTag) => HexString;
|
|
154
|
+
eth_getProof: (address: string, storageKeys: string[], block?: ELBlockNumberOrTag) => ELProof;
|
|
155
|
+
eth_getBlockByNumber: (block: ELBlockNumberOrTag, hydrated?: boolean) => ELBlock | undefined;
|
|
156
|
+
eth_getBlockByHash: (block: string, hydrated?: boolean) => ELBlock | undefined;
|
|
157
|
+
};
|
|
158
|
+
export type ELApiParams = {
|
|
159
|
+
[K in keyof ELApi]: Parameters<ELApi[K]>;
|
|
160
|
+
};
|
|
161
|
+
export type ELApiReturn = {
|
|
162
|
+
[K in keyof ELApi]: ReturnType<ELApi[K]>;
|
|
163
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {Lightclient} from "@lodestar/light-client";
|
|
2
|
+
|
|
3
|
+
export function assertLightClient(client?: Lightclient): asserts client is Lightclient {
|
|
4
|
+
if (!client) {
|
|
5
|
+
throw new Error("Light client is not initialized yet.");
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isTruthy<T = unknown>(value: T): value is Exclude<T, undefined | null> {
|
|
10
|
+
return value !== undefined && value !== null && value !== false;
|
|
11
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {ApiClient} from "@lodestar/api/beacon";
|
|
2
|
+
import {GenesisData, Lightclient} from "@lodestar/light-client";
|
|
3
|
+
import {Bytes32, ExecutionPayload, capella} from "@lodestar/types";
|
|
4
|
+
import {Logger} from "@lodestar/utils";
|
|
5
|
+
import {MAX_PAYLOAD_HISTORY} from "../constants.js";
|
|
6
|
+
import {hexToBuffer} from "./conversion.js";
|
|
7
|
+
|
|
8
|
+
export async function fetchBlock(api: ApiClient, slot: number): Promise<capella.SignedBeaconBlock | undefined> {
|
|
9
|
+
const res = await api.beacon.getBlockV2({blockId: slot});
|
|
10
|
+
|
|
11
|
+
if (res.ok) return res.value() as capella.SignedBeaconBlock;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function fetchNearestBlock(
|
|
16
|
+
api: ApiClient,
|
|
17
|
+
slot: number,
|
|
18
|
+
direction: "up" | "down" = "down"
|
|
19
|
+
): Promise<capella.SignedBeaconBlock> {
|
|
20
|
+
const res = await api.beacon.getBlockV2({blockId: slot});
|
|
21
|
+
|
|
22
|
+
if (res.ok) return res.value() as capella.SignedBeaconBlock;
|
|
23
|
+
|
|
24
|
+
if (!res.ok && res.status === 404) {
|
|
25
|
+
return fetchNearestBlock(api, direction === "down" ? slot - 1 : slot + 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error(`Can not fetch nearest block for slot=${slot}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getUnFinalizedRangeForPayloads(lightClient: Lightclient): Promise<{start: number; end: number}> {
|
|
32
|
+
const headSlot = lightClient.getHead().beacon.slot;
|
|
33
|
+
const finalizeSlot = lightClient.getFinalized().beacon.slot;
|
|
34
|
+
const endSlot = headSlot - MAX_PAYLOAD_HISTORY;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
start: headSlot,
|
|
38
|
+
end: endSlot < finalizeSlot ? finalizeSlot : endSlot,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function getExecutionPayloads({
|
|
43
|
+
api,
|
|
44
|
+
startSlot,
|
|
45
|
+
endSlot,
|
|
46
|
+
logger,
|
|
47
|
+
}: {
|
|
48
|
+
api: ApiClient;
|
|
49
|
+
startSlot: number;
|
|
50
|
+
endSlot: number;
|
|
51
|
+
logger: Logger;
|
|
52
|
+
}): Promise<Map<number, ExecutionPayload>> {
|
|
53
|
+
[startSlot, endSlot] = [Math.min(startSlot, endSlot), Math.max(startSlot, endSlot)];
|
|
54
|
+
if (startSlot === endSlot) {
|
|
55
|
+
logger.debug("Fetching EL payload", {slot: startSlot});
|
|
56
|
+
} else {
|
|
57
|
+
logger.debug("Fetching EL payloads", {startSlot, endSlot});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const payloads = new Map<number, ExecutionPayload>();
|
|
61
|
+
|
|
62
|
+
let slot = endSlot;
|
|
63
|
+
let block = await fetchNearestBlock(api, slot);
|
|
64
|
+
payloads.set(block.message.slot, block.message.body.executionPayload);
|
|
65
|
+
slot = block.message.slot - 1;
|
|
66
|
+
|
|
67
|
+
while (slot >= startSlot) {
|
|
68
|
+
const previousBlock = await fetchNearestBlock(api, block.message.slot - 1);
|
|
69
|
+
|
|
70
|
+
if (block.message.body.executionPayload.parentHash === previousBlock.message.body.executionPayload.blockHash) {
|
|
71
|
+
payloads.set(block.message.slot, block.message.body.executionPayload);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
slot = block.message.slot - 1;
|
|
75
|
+
block = previousBlock;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return payloads;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function getExecutionPayloadForBlockNumber(
|
|
82
|
+
api: ApiClient,
|
|
83
|
+
startSlot: number,
|
|
84
|
+
blockNumber: number
|
|
85
|
+
): Promise<Map<number, ExecutionPayload>> {
|
|
86
|
+
const payloads = new Map<number, ExecutionPayload>();
|
|
87
|
+
|
|
88
|
+
let block = await fetchNearestBlock(api, startSlot);
|
|
89
|
+
payloads.set(block.message.slot, block.message.body.executionPayload);
|
|
90
|
+
|
|
91
|
+
while (payloads.get(block.message.slot)?.blockNumber !== blockNumber) {
|
|
92
|
+
const previousBlock = await fetchNearestBlock(api, block.message.slot - 1);
|
|
93
|
+
block = previousBlock;
|
|
94
|
+
payloads.set(block.message.slot, block.message.body.executionPayload);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return payloads;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function getGenesisData(api: Pick<ApiClient, "beacon">): Promise<GenesisData> {
|
|
101
|
+
const {genesisTime, genesisValidatorsRoot} = (await api.beacon.getGenesis()).value();
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
genesisTime,
|
|
105
|
+
genesisValidatorsRoot,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getSyncCheckpoint(api: Pick<ApiClient, "beacon">, checkpoint?: string): Promise<Bytes32> {
|
|
110
|
+
let syncCheckpoint: Bytes32 | undefined = checkpoint ? hexToBuffer(checkpoint) : undefined;
|
|
111
|
+
|
|
112
|
+
if (syncCheckpoint && syncCheckpoint.byteLength !== 32) {
|
|
113
|
+
throw Error(`Checkpoint root must be 32 bytes. length=${syncCheckpoint.byteLength}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!syncCheckpoint) {
|
|
117
|
+
const res = await api.beacon.getStateFinalityCheckpoints({stateId: "head"});
|
|
118
|
+
syncCheckpoint = res.value().finalized.root;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return syncCheckpoint;
|
|
122
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {BlockData, HeaderData} from "@ethereumjs/block";
|
|
2
|
+
import {ELBlock, ELTransaction} from "../types.js";
|
|
3
|
+
import {isTruthy} from "./assertion.js";
|
|
4
|
+
|
|
5
|
+
export function numberToHex(num: number | bigint): string {
|
|
6
|
+
return "0x" + num.toString(16);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function hexToNumber(num: string): number {
|
|
10
|
+
return num.startsWith("0x") ? parseInt(num.slice(2), 16) : parseInt(num, 16);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hexToBigInt(num: string): bigint {
|
|
14
|
+
return num.startsWith("0x") ? BigInt(num) : BigInt(`0x${num}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function bigIntToHex(num: bigint): string {
|
|
18
|
+
return `0x${num.toString(16)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function bufferToHex(buffer: Buffer | Uint8Array): string {
|
|
22
|
+
return "0x" + Buffer.from(buffer).toString("hex");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hexToBuffer(val: string): Buffer {
|
|
26
|
+
const hexWithEvenLength = val.length % 2 ? `0${val}` : val;
|
|
27
|
+
return Buffer.from(hexWithEvenLength.replace("0x", ""), "hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function padLeft<T extends Buffer | Uint8Array>(v: T, length: number): T {
|
|
31
|
+
const buf = Buffer.alloc(length);
|
|
32
|
+
Buffer.from(v).copy(buf, length - v.length);
|
|
33
|
+
|
|
34
|
+
if (Buffer.isBuffer(v)) return buf as T;
|
|
35
|
+
|
|
36
|
+
return Uint8Array.from(buf) as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function headerDataFromELBlock(blockInfo: ELBlock): HeaderData {
|
|
40
|
+
return {
|
|
41
|
+
parentHash: blockInfo.parentHash,
|
|
42
|
+
uncleHash: blockInfo.sha3Uncles,
|
|
43
|
+
coinbase: blockInfo.miner,
|
|
44
|
+
stateRoot: blockInfo.stateRoot,
|
|
45
|
+
transactionsTrie: blockInfo.transactionsRoot,
|
|
46
|
+
receiptTrie: blockInfo.receiptsRoot,
|
|
47
|
+
logsBloom: blockInfo.logsBloom,
|
|
48
|
+
difficulty: BigInt(blockInfo.difficulty),
|
|
49
|
+
number: BigInt(blockInfo.number),
|
|
50
|
+
gasLimit: BigInt(blockInfo.gasLimit),
|
|
51
|
+
gasUsed: BigInt(blockInfo.gasUsed),
|
|
52
|
+
timestamp: BigInt(blockInfo.timestamp),
|
|
53
|
+
extraData: blockInfo.extraData,
|
|
54
|
+
mixHash: blockInfo.mixHash, // some reason the types are not up to date :(
|
|
55
|
+
nonce: blockInfo.nonce,
|
|
56
|
+
baseFeePerGas: blockInfo.baseFeePerGas ? BigInt(blockInfo.baseFeePerGas) : undefined,
|
|
57
|
+
withdrawalsRoot: blockInfo.withdrawalsRoot ?? undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function txDataFromELBlock(txInfo: ELTransaction) {
|
|
62
|
+
return {
|
|
63
|
+
...txInfo,
|
|
64
|
+
data: txInfo.input,
|
|
65
|
+
gasPrice: isTruthy(txInfo.gasPrice) ? BigInt(txInfo.gasPrice) : null,
|
|
66
|
+
gasLimit: txInfo.gas,
|
|
67
|
+
to: isTruthy(txInfo.to) ? padLeft(hexToBuffer(txInfo.to), 20) : undefined,
|
|
68
|
+
value: txInfo.value ? BigInt(txInfo.value) : undefined,
|
|
69
|
+
maxFeePerGas: isTruthy(txInfo.maxFeePerGas) ? BigInt(txInfo.maxFeePerGas) : undefined,
|
|
70
|
+
maxPriorityFeePerGas: isTruthy(txInfo.maxPriorityFeePerGas) ? BigInt(txInfo.maxPriorityFeePerGas) : undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function blockDataFromELBlock(blockInfo: ELBlock): BlockData {
|
|
75
|
+
return {
|
|
76
|
+
header: headerDataFromELBlock(blockInfo),
|
|
77
|
+
transactions: blockInfo.transactions.map(txDataFromELBlock) as BlockData["transactions"],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function cleanObject<T extends Record<string, unknown> | unknown[]>(obj: T): T {
|
|
82
|
+
const isNullify = (v: unknown): boolean => v === undefined || v === null;
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(obj)) return obj.filter((v) => isNullify(v)) as T;
|
|
85
|
+
|
|
86
|
+
if (typeof obj === "object") {
|
|
87
|
+
for (const key of Object.keys(obj)) {
|
|
88
|
+
if (isNullify(obj[key])) {
|
|
89
|
+
delete obj[key];
|
|
90
|
+
} else if (typeof obj[key] === "object") {
|
|
91
|
+
cleanObject(obj[key] as Record<string, unknown>);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return obj;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convert an array to array of chunks of length N
|
|
101
|
+
* @example
|
|
102
|
+
* chunkIntoN([1,2,3,4,5,6], 2)
|
|
103
|
+
* => [[1,2], [3,4], [5,6]]
|
|
104
|
+
*/
|
|
105
|
+
export function chunkIntoN<T extends unknown[]>(arr: T, n: number): T[] {
|
|
106
|
+
return Array.from({length: Math.ceil(arr.length / n)}, (_, i) => arr.slice(i * n, i * n + n)) as T[];
|
|
107
|
+
}
|
package/src/utils/evm.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import {Block, BlockHeader} from "@ethereumjs/block";
|
|
2
|
+
import {Blockchain} from "@ethereumjs/blockchain";
|
|
3
|
+
import {TransactionFactory} from "@ethereumjs/tx";
|
|
4
|
+
import {Account, Address} from "@ethereumjs/util";
|
|
5
|
+
import {RunTxResult, VM} from "@ethereumjs/vm";
|
|
6
|
+
import {NetworkName} from "@lodestar/config/networks";
|
|
7
|
+
import {ExecutionPayload} from "@lodestar/types";
|
|
8
|
+
import {Logger} from "@lodestar/utils";
|
|
9
|
+
import {ZERO_ADDRESS} from "../constants.js";
|
|
10
|
+
import {ProofProvider} from "../proof_provider/proof_provider.js";
|
|
11
|
+
import {ELBlock, ELProof, ELTransaction, JsonRpcVersion} from "../types.js";
|
|
12
|
+
import {bufferToHex, chunkIntoN, cleanObject, hexToBigInt, hexToBuffer, numberToHex, padLeft} from "./conversion.js";
|
|
13
|
+
import {getChainCommon, getTxType} from "./execution.js";
|
|
14
|
+
import {isValidResponse} from "./json_rpc.js";
|
|
15
|
+
import {ELRpcProvider} from "./rpc_provider.js";
|
|
16
|
+
import {isNullish, isValidAccount, isValidCodeHash, isValidStorageKeys} from "./validation.js";
|
|
17
|
+
|
|
18
|
+
export async function createVM({proofProvider}: {proofProvider: ProofProvider}): Promise<VM> {
|
|
19
|
+
const common = getChainCommon(proofProvider.config.PRESET_BASE as string);
|
|
20
|
+
const blockchain = await Blockchain.create({common});
|
|
21
|
+
|
|
22
|
+
// Connect blockchain object with existing proof provider for block history
|
|
23
|
+
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
|
|
24
|
+
(blockchain as any).getBlock = async (blockId: number) => {
|
|
25
|
+
const payload = await proofProvider.getExecutionPayload(blockId);
|
|
26
|
+
return {
|
|
27
|
+
hash: () => payload.blockHash,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const vm = await VM.create({common, blockchain});
|
|
31
|
+
|
|
32
|
+
return vm;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getVMWithState({
|
|
36
|
+
rpc,
|
|
37
|
+
executionPayload,
|
|
38
|
+
tx,
|
|
39
|
+
vm,
|
|
40
|
+
logger,
|
|
41
|
+
}: {
|
|
42
|
+
rpc: ELRpcProvider;
|
|
43
|
+
vm: VM;
|
|
44
|
+
executionPayload: ExecutionPayload;
|
|
45
|
+
tx: ELTransaction;
|
|
46
|
+
logger: Logger;
|
|
47
|
+
}): Promise<VM> {
|
|
48
|
+
const {stateRoot, blockHash, gasLimit} = executionPayload;
|
|
49
|
+
const blockHashHex = bufferToHex(blockHash);
|
|
50
|
+
|
|
51
|
+
// If tx does not have a from address then it must be initiated via zero address
|
|
52
|
+
const from = tx.from ?? ZERO_ADDRESS;
|
|
53
|
+
const to = tx.to;
|
|
54
|
+
|
|
55
|
+
// Create Access List for the contract call
|
|
56
|
+
const accessListTx = cleanObject({
|
|
57
|
+
to,
|
|
58
|
+
from,
|
|
59
|
+
data: tx.input ? tx.input : tx.data,
|
|
60
|
+
value: tx.value,
|
|
61
|
+
gas: tx.gas ? tx.gas : numberToHex(gasLimit),
|
|
62
|
+
gasPrice: "0x0",
|
|
63
|
+
}) as ELTransaction;
|
|
64
|
+
const response = await rpc.request("eth_createAccessList", [accessListTx, blockHashHex], {raiseError: false});
|
|
65
|
+
|
|
66
|
+
if (!isValidResponse(response) || response.result.error) {
|
|
67
|
+
throw new Error(`Invalid response from RPC. method: eth_createAccessList, params: ${JSON.stringify(tx)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const storageKeysMap: Record<string, string[]> = {};
|
|
71
|
+
for (const {address, storageKeys} of response.result.accessList) {
|
|
72
|
+
storageKeysMap[address] = storageKeys;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If from address is not present then we have to fetch it for all keys
|
|
76
|
+
if (isNullish(storageKeysMap[from])) {
|
|
77
|
+
storageKeysMap[from] = [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If to address is not present then we have to fetch it with for all keys
|
|
81
|
+
if (to && isNullish(storageKeysMap[to])) {
|
|
82
|
+
storageKeysMap[to] = [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const batchRequests = [];
|
|
86
|
+
for (const [address, storageKeys] of Object.entries(storageKeysMap)) {
|
|
87
|
+
batchRequests.push({
|
|
88
|
+
jsonrpc: "2.0" as JsonRpcVersion,
|
|
89
|
+
id: rpc.getRequestId(),
|
|
90
|
+
method: "eth_getProof",
|
|
91
|
+
params: [address, storageKeys, blockHashHex],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
batchRequests.push({
|
|
95
|
+
jsonrpc: "2.0" as JsonRpcVersion,
|
|
96
|
+
id: rpc.getRequestId(),
|
|
97
|
+
method: "eth_getCode",
|
|
98
|
+
params: [address, blockHashHex],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If all responses are valid then we will have even number of responses
|
|
103
|
+
// For each address, one response for eth_getProof and one for eth_getCode
|
|
104
|
+
const batchResponse = await rpc.batchRequest(batchRequests, {raiseError: true});
|
|
105
|
+
const batchResponseInChunks = chunkIntoN(batchResponse, 2);
|
|
106
|
+
|
|
107
|
+
const vmState: VMState = {};
|
|
108
|
+
for (const [proofResponse, codeResponse] of batchResponseInChunks) {
|
|
109
|
+
const addressHex = proofResponse.request.params[0] as string;
|
|
110
|
+
if (!isNullish(vmState[addressHex])) continue;
|
|
111
|
+
|
|
112
|
+
const proof = proofResponse.response.result as ELProof;
|
|
113
|
+
const storageKeys = proofResponse.request.params[1] as string[];
|
|
114
|
+
const code = codeResponse.response.result as string;
|
|
115
|
+
|
|
116
|
+
const validAccount = await isValidAccount({address: addressHex, proof, logger, stateRoot});
|
|
117
|
+
const validStorage = validAccount && (await isValidStorageKeys({storageKeys, proof, logger}));
|
|
118
|
+
if (!validAccount || !validStorage) {
|
|
119
|
+
throw new Error(`Invalid account: ${addressHex}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!(await isValidCodeHash({codeResponse: code, logger, codeHash: proof.codeHash}))) {
|
|
123
|
+
throw new Error(`Invalid code hash: ${addressHex}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
vmState[addressHex] = {code, proof};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return updateVMWithState({vm, state: vmState, logger});
|
|
130
|
+
}
|
|
131
|
+
type VMState = Record<string, {code: string; proof: ELProof}>;
|
|
132
|
+
export async function updateVMWithState({vm, state}: {logger: Logger; state: VMState; vm: VM}): Promise<VM> {
|
|
133
|
+
await vm.stateManager.checkpoint();
|
|
134
|
+
for (const [addressHex, {proof, code}] of Object.entries(state)) {
|
|
135
|
+
const address = Address.fromString(addressHex);
|
|
136
|
+
const codeBuffer = hexToBuffer(code);
|
|
137
|
+
|
|
138
|
+
const account = Account.fromAccountData({
|
|
139
|
+
nonce: BigInt(proof.nonce),
|
|
140
|
+
balance: BigInt(proof.balance),
|
|
141
|
+
codeHash: proof.codeHash,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await vm.stateManager.putAccount(address, account);
|
|
145
|
+
|
|
146
|
+
for (const {key, value} of proof.storageProof) {
|
|
147
|
+
await vm.stateManager.putContractStorage(address, padLeft(hexToBuffer(key), 32), padLeft(hexToBuffer(value), 32));
|
|
148
|
+
}
|
|
149
|
+
if (codeBuffer.byteLength !== 0) await vm.stateManager.putContractCode(address, codeBuffer);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await vm.stateManager.commit();
|
|
153
|
+
return vm;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function executeVMCall({
|
|
157
|
+
rpc,
|
|
158
|
+
tx,
|
|
159
|
+
vm,
|
|
160
|
+
executionPayload,
|
|
161
|
+
network,
|
|
162
|
+
}: {
|
|
163
|
+
rpc: ELRpcProvider;
|
|
164
|
+
tx: ELTransaction;
|
|
165
|
+
vm: VM;
|
|
166
|
+
executionPayload: ExecutionPayload;
|
|
167
|
+
network: NetworkName;
|
|
168
|
+
}): Promise<RunTxResult["execResult"]> {
|
|
169
|
+
const {from, to, gas, gasPrice, maxPriorityFeePerGas, value, data, input} = tx;
|
|
170
|
+
const blockHash = bufferToHex(executionPayload.blockHash);
|
|
171
|
+
const {result: block} = await rpc.request("eth_getBlockByHash", [blockHash, true], {
|
|
172
|
+
raiseError: true,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!block) {
|
|
176
|
+
throw new Error(`Block not found: ${blockHash}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const {execResult} = await vm.evm.runCall({
|
|
180
|
+
caller: from ? Address.fromString(from) : undefined,
|
|
181
|
+
to: to ? Address.fromString(to) : undefined,
|
|
182
|
+
gasLimit: hexToBigInt(gas ?? block.gasLimit),
|
|
183
|
+
gasPrice: hexToBigInt(gasPrice ?? maxPriorityFeePerGas ?? "0x0"),
|
|
184
|
+
value: hexToBigInt(value ?? "0x0"),
|
|
185
|
+
data: input ? hexToBuffer(input) : data ? hexToBuffer(data) : undefined,
|
|
186
|
+
block: {
|
|
187
|
+
header: getVMBlockHeaderFromELBlock(block, executionPayload, network),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (execResult.exceptionError) {
|
|
192
|
+
throw new Error(execResult.exceptionError.error);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return execResult;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function executeVMTx({
|
|
199
|
+
rpc,
|
|
200
|
+
tx,
|
|
201
|
+
vm,
|
|
202
|
+
executionPayload,
|
|
203
|
+
network,
|
|
204
|
+
}: {
|
|
205
|
+
rpc: ELRpcProvider;
|
|
206
|
+
tx: ELTransaction;
|
|
207
|
+
vm: VM;
|
|
208
|
+
executionPayload: ExecutionPayload;
|
|
209
|
+
network: NetworkName;
|
|
210
|
+
}): Promise<RunTxResult> {
|
|
211
|
+
const {result: block} = await rpc.request("eth_getBlockByHash", [bufferToHex(executionPayload.blockHash), true], {
|
|
212
|
+
raiseError: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!block) {
|
|
216
|
+
throw new Error(`Block not found: ${bufferToHex(executionPayload.blockHash)}`);
|
|
217
|
+
}
|
|
218
|
+
const txType = getTxType(tx);
|
|
219
|
+
const from = tx.from ? Address.fromString(tx.from) : Address.zero();
|
|
220
|
+
const to = tx.to ? Address.fromString(tx.to) : undefined;
|
|
221
|
+
|
|
222
|
+
const txData = {
|
|
223
|
+
...tx,
|
|
224
|
+
from,
|
|
225
|
+
to,
|
|
226
|
+
type: txType,
|
|
227
|
+
// If no gas limit is specified use the last block gas limit as an upper bound.
|
|
228
|
+
gasLimit: hexToBigInt(tx.gas ?? block.gasLimit),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (txType === 2) {
|
|
232
|
+
// Handle EIP-1559 transactions
|
|
233
|
+
// To fix the vm error: Transaction's maxFeePerGas (0) is less than the block's baseFeePerGas
|
|
234
|
+
txData.maxFeePerGas = txData.maxFeePerGas ?? block.baseFeePerGas;
|
|
235
|
+
} else {
|
|
236
|
+
// Legacy transaction
|
|
237
|
+
txData.gasPrice = isNullish(txData.gasPrice) || txData.gasPrice === "0x0" ? block.baseFeePerGas : txData.gasPrice;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const txObject = TransactionFactory.fromTxData(txData, {common: getChainCommon(network), freeze: false});
|
|
241
|
+
|
|
242
|
+
// Override to avoid tx signature verification
|
|
243
|
+
txObject.getSenderAddress = () => (tx.from ? Address.fromString(tx.from) : Address.zero());
|
|
244
|
+
|
|
245
|
+
const result = await vm.runTx({
|
|
246
|
+
tx: txObject,
|
|
247
|
+
skipNonce: true,
|
|
248
|
+
skipBalance: true,
|
|
249
|
+
skipBlockGasLimitValidation: true,
|
|
250
|
+
skipHardForkValidation: true,
|
|
251
|
+
block: {
|
|
252
|
+
header: getVMBlockHeaderFromELBlock(block, executionPayload, network),
|
|
253
|
+
} as Block,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function getVMBlockHeaderFromELBlock(
|
|
260
|
+
block: ELBlock,
|
|
261
|
+
executionPayload: ExecutionPayload,
|
|
262
|
+
network: NetworkName
|
|
263
|
+
): BlockHeader {
|
|
264
|
+
const blockHeaderData = {
|
|
265
|
+
number: hexToBigInt(block.number),
|
|
266
|
+
cliqueSigner: () => Address.fromString(block.miner),
|
|
267
|
+
timestamp: hexToBigInt(block.timestamp),
|
|
268
|
+
difficulty: hexToBigInt(block.difficulty),
|
|
269
|
+
gasLimit: hexToBigInt(block.gasLimit),
|
|
270
|
+
baseFeePerGas: block.baseFeePerGas ? hexToBigInt(block.baseFeePerGas) : undefined,
|
|
271
|
+
|
|
272
|
+
// Use these values from the execution payload
|
|
273
|
+
// instead of the block values to ensure that
|
|
274
|
+
// the VM is using the verified values from the lightclient
|
|
275
|
+
prevRandao: Buffer.from(executionPayload.prevRandao),
|
|
276
|
+
stateRoot: Buffer.from(executionPayload.stateRoot),
|
|
277
|
+
parentHash: Buffer.from(executionPayload.parentHash),
|
|
278
|
+
|
|
279
|
+
// TODO: Fix the coinbase address
|
|
280
|
+
coinbase: Address.fromString(block.miner),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return BlockHeader.fromHeaderData(blockHeaderData, {common: getChainCommon(network)});
|
|
284
|
+
}
|