Skip to content

Commit e8fcfc2

Browse files
committed
lodestar prover added, periodic bug with multicall account proof
1 parent d358e18 commit e8fcfc2

File tree

8 files changed

+551
-1928
lines changed

8 files changed

+551
-1928
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/

package-lock.json

Lines changed: 356 additions & 1835 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,36 @@
1919
},
2020
"author": "",
2121
"license": "ISC",
22+
"overrides": {
23+
"@ethereumjs/block": "^5.0.1",
24+
"@ethereumjs/blockchain": "^7.0.1",
25+
"@ethereumjs/client": "^0.9.0",
26+
"@ethereumjs/common": "^4.1.0",
27+
"@ethereumjs/evm": "^2.1.0",
28+
"@ethereumjs/genesis": "^0.2.0",
29+
"@ethereumjs/rlp": "^5.0.1",
30+
"@ethereumjs/statemanager": "^2.1.0",
31+
"@ethereumjs/trie": "^6.0.1",
32+
"@ethereumjs/tx": "^5.1.0",
33+
"@ethereumjs/util": "^9.0.1",
34+
"@ethereumjs/vm": "^7.1.0"
35+
},
2236
"dependencies": {
23-
"@chainsafe/ssz": "^0.13.0",
24-
"@ethereumjs/statemanager": "^2.0.0",
25-
"@ethereumjs/trie": "^6.0.0",
26-
"@ethersproject/abi": "^5.7.0",
27-
"@lodestar/api": "^1.11.1",
28-
"@lodestar/config": "^1.11.1",
29-
"@lodestar/light-client": "^1.11.1",
37+
"@lodestar/config": "^1.12.0",
38+
"@lodestar/light-client": "^1.12.0",
3039
"@lodestar/prover": "^1.12.0",
3140
"@lodestar/types": "^1.12.0",
3241
"@tippyjs/react": "^4.2.6",
3342
"@types/debug": "^4.1.8",
3443
"assert": "^2.0.0",
3544
"bignumber.js": "^9.1.2",
3645
"buffer": "^6.0.3",
37-
"ethereumjs-util": "^7.1.5",
46+
"ethereum-multicall": "^2.21.0",
3847
"ethers": "^6.7.1",
3948
"react": "^18.2.0",
4049
"react-dom": "^18.2.0",
4150
"react-tooltip": "^5.22.0",
42-
"web3": "^4.2.2",
51+
"web3": "^4.2.0",
4352
"web3-utils": "^4.0.6"
4453
},
4554
"devDependencies": {
@@ -86,7 +95,6 @@
8695
"ts-node": "^10.9.1",
8796
"typescript": "^5.2.2",
8897
"webpack": "^5.88.2",
89-
"webpack-cli": "^5.1.4",
90-
"webpack-node-externals": "^3.0.0"
98+
"webpack-cli": "^5.1.4"
9199
}
92100
}

patches/@lodestar+prover+1.12.0.patch

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,56 @@ index f96ad5d..697a06b 100644
1111
}
1212
export function padLeft(v, length) {
1313
const buf = Buffer.alloc(length);
14+
diff --git a/node_modules/@lodestar/prover/lib/utils/evm.js b/node_modules/@lodestar/prover/lib/utils/evm.js
15+
index 1fdc555..c44186a 100644
16+
--- a/node_modules/@lodestar/prover/lib/utils/evm.js
17+
+++ b/node_modules/@lodestar/prover/lib/utils/evm.js
18+
@@ -32,7 +32,7 @@ export async function getVMWithState({ rpc, executionPayload, tx, vm, logger, })
19+
const accessListTx = cleanObject({
20+
to,
21+
from,
22+
- data: tx.data,
23+
+ data: tx.input? tx.input : tx.data,
24+
value: tx.value,
25+
gas: tx.gas ? tx.gas : numberToHex(gasLimit),
26+
gasPrice: "0x0",
27+
diff --git a/node_modules/@lodestar/prover/lib/utils/validation.js b/node_modules/@lodestar/prover/lib/utils/validation.js
28+
index b5454bc..a46412d 100644
29+
--- a/node_modules/@lodestar/prover/lib/utils/validation.js
30+
+++ b/node_modules/@lodestar/prover/lib/utils/validation.js
31+
@@ -1,7 +1,7 @@
32+
import { Block } from "@ethereumjs/block";
33+
import { RLP } from "@ethereumjs/rlp";
34+
import { Trie } from "@ethereumjs/trie";
35+
-import { Account, KECCAK256_NULL_S } from "@ethereumjs/util";
36+
+import { Account, KECCAK256_NULL_S, equalsBytes } from "@ethereumjs/util";
37+
import { keccak256 } from "ethereum-cryptography/keccak.js";
38+
import { blockDataFromELBlock, bufferToHex, hexToBuffer, padLeft } from "./conversion.js";
39+
import { getChainCommon } from "./execution.js";
40+
@@ -26,7 +26,7 @@ export async function isValidAccount({ address, stateRoot, proof, logger, }) {
41+
storageRoot: proof.storageHash,
42+
codeHash: proof.codeHash,
43+
});
44+
- return account.serialize().equals(expectedAccountRLP ? expectedAccountRLP : emptyAccountSerialize);
45+
+ return equalsBytes(account.serialize(), expectedAccountRLP ? expectedAccountRLP : emptyAccountSerialize);
46+
}
47+
catch (err) {
48+
logger.error("Error verifying account proof", undefined, err);
49+
@@ -43,7 +43,7 @@ export async function isValidStorageKeys({ storageKeys, proof, logger, }) {
50+
// buffer.equals is not compatible with Uint8Array for browser
51+
// so we need to convert the output of RLP.encode to Buffer first
52+
const isStorageValid = (!expectedStorageRLP && sp.value === "0x0") ||
53+
- (!!expectedStorageRLP && expectedStorageRLP.equals(Buffer.from(RLP.encode(sp.value))));
54+
+ (!!expectedStorageRLP && equalsBytes(expectedStorageRLP, RLP.encode(sp.value)));
55+
if (!isStorageValid)
56+
return false;
57+
}
58+
@@ -56,7 +56,7 @@ export async function isValidStorageKeys({ storageKeys, proof, logger, }) {
59+
}
60+
export async function isValidBlock({ executionPayload, block, logger, config, }) {
61+
const common = getChainCommon(config.PRESET_BASE);
62+
- common.setHardforkByBlockNumber(executionPayload.blockNumber, undefined, executionPayload.timestamp);
63+
+ common.setHardforkBy({ blockNumber: executionPayload.blockNumber, timestamp: executionPayload.timestamp })
64+
const blockObject = Block.fromBlockData(blockDataFromELBlock(block), { common });
65+
if (bufferToHex(executionPayload.blockHash) !== bufferToHex(blockObject.hash())) {
66+
logger.error("Block hash does not match", {

src/LightClientVerifier.ts

Lines changed: 98 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
1-
import { fromHexString, toHexString } from '@chainsafe/ssz';
2-
import { DefaultStateManager } from '@ethereumjs/statemanager';
3-
import { defaultAbiCoder } from '@ethersproject/abi';
4-
import { Api, ApiError, getClient } from '@lodestar/api';
5-
import { createChainForkConfig } from '@lodestar/config';
6-
import { genesisData as networkGenesis, NetworkName, networksChainConfig } from '@lodestar/config/networks';
7-
import { Lightclient, LightclientEvent, RunStatusCode } from '@lodestar/light-client';
8-
import { LightClientRestTransport } from '@lodestar/light-client/transport';
9-
import { getLcLoggerConsole } from '@lodestar/light-client/utils';
10-
import { allForks, bellatrix, ssz } from '@lodestar/types';
11-
import { keccak256, toBuffer } from 'ethereumjs-util';
1+
import { NetworkName } from '@lodestar/config/networks';
2+
import { LightclientEvent } from '@lodestar/light-client';
3+
4+
import { allForks } from '@lodestar/types';
125
import { formatUnits } from 'ethers';
136
import Web3 from 'web3';
14-
import { hexToNumberString, numberToHex } from 'web3-utils';
157
import { createVerifiedExecutionProvider, LCTransport, Web3jsProvider, ProofProvider } from '@lodestar/prover';
8+
import { hexToNumberString } from 'web3-utils';
9+
import { Multicall, ContractCallResults, ContractCallContext } from 'ethereum-multicall';
1610

17-
const balanceOfABI = [
11+
const ERC20ABI = [
1812
{
1913
constant: true,
2014
inputs: [
@@ -34,6 +28,19 @@ const balanceOfABI = [
3428
stateMutability: 'view',
3529
type: 'function',
3630
},
31+
{
32+
constant: true,
33+
inputs: [],
34+
name: 'decimals',
35+
outputs: [
36+
{
37+
name: 'decimals',
38+
type: 'uint8',
39+
},
40+
],
41+
payable: false,
42+
type: 'function',
43+
},
3744
];
3845

3946
export type ERC20Contract = {
@@ -52,7 +59,6 @@ export type LightClientVerifierInitArgs = {
5259
network: NetworkName;
5360
elRpcUrl: string;
5461
beaconApiUrl: string;
55-
erc20Contracts: Record<string, ERC20Contract>;
5662
initialCheckpoint: string;
5763
};
5864

@@ -94,22 +100,18 @@ export type AccountsToVerify = Record<string, AccountBalance>;
94100

95101
export class LightClientVerifier {
96102
private web3: Web3 | undefined;
97-
private stateManager: DefaultStateManager;
98-
private erc20Contracts: Record<string, ERC20Contract>;
99103
private network: NetworkName;
100104
private beaconApiUrl: string;
101105
private initialCheckpoint: string;
102106
private elRpcUrl: string;
103107
public provider: Web3jsProvider | undefined;
104108
public proofProvider: ProofProvider | undefined;
105109

106-
constructor({ network, elRpcUrl, beaconApiUrl, erc20Contracts, initialCheckpoint }: LightClientVerifierInitArgs) {
110+
constructor({ network, elRpcUrl, beaconApiUrl, initialCheckpoint }: LightClientVerifierInitArgs) {
107111
this.elRpcUrl = elRpcUrl;
108112
this.beaconApiUrl = beaconApiUrl;
109113
this.network = network;
110-
this.erc20Contracts = erc20Contracts;
111114
this.initialCheckpoint = initialCheckpoint;
112-
this.stateManager = new DefaultStateManager();
113115
}
114116

115117
public async verifyBalances(
@@ -119,14 +121,8 @@ export class LightClientVerifier {
119121
): Promise<BalanceVerificationResult> {
120122
const accountsResult: Record<string, BalanceComparisonAtBlock> = {};
121123
for (const [address, balance] of Object.entries(accountsToVerify)) {
122-
const matchingContracts: ERC20Contract[] = [];
123-
Object.entries(this.erc20Contracts).forEach(([address, erc20Contract]) => {
124-
if (Object.keys(balance.erc20Balances).includes(address)) {
125-
matchingContracts.push(erc20Contract);
126-
}
127-
});
128-
129-
const accountResult = await this.fetchAndVerifyAccount(address, matchingContracts);
124+
const erc20Addresses: string[] = Object.keys(balance.erc20Balances);
125+
const accountResult = await this.fetchAndVerifyAccount(address, erc20Addresses);
130126
const balanceComparisonResult = this.compareBalances(
131127
balance,
132128
accountResult?.balance,
@@ -213,22 +209,23 @@ export class LightClientVerifier {
213209
this.provider = provider;
214210
this.proofProvider = proofProvider;
215211
this.web3 = new Web3(provider);
212+
await this.waitForClientToStart();
216213
}
217214

218215
public async stop() {
219216
await this.proofProvider?.lightClient?.stop();
220217
}
221218

222-
private async waitForClientStarted(): Promise<void> {
219+
private async waitForClientToStart(): Promise<void> {
223220
await this.proofProvider?.waitToBeReady();
224221
}
225222

226223
public setOptimisticHeaderHook(handler: (newHeader: allForks.LightClientHeader) => void) {
227224
this.proofProvider?.lightClient!.emitter.on(LightclientEvent.lightClientOptimisticHeader, handler);
228225
}
229226

230-
private async fetchAndVerifyAccount(address: string, erc20Contracts: ERC20Contract[]) {
231-
await this.waitForClientStarted();
227+
private async fetchAndVerifyAccount(address: string, erc20Contracts: string[]) {
228+
await this.waitForClientToStart();
232229
const verifiedAccount = await this.fetchAndVerifyAddressBalances({
233230
address,
234231
erc20Contracts,
@@ -242,25 +239,83 @@ export class LightClientVerifier {
242239
erc20Contracts,
243240
}: {
244241
address: string;
245-
erc20Contracts: ERC20Contract[];
242+
erc20Contracts: string[];
246243
}): Promise<VerifiedAccount> {
247-
const ethBalance = await this.web3!.eth.getBalance(address);
244+
let ethBalance;
245+
try {
246+
let balance = await this.web3!.eth.getBalance(address);
247+
console.log('eth', balance);
248+
ethBalance = {
249+
balance: Number(this.web3!.utils.fromWei(balance, 'ether')),
250+
verified: true,
251+
};
252+
} catch (e) {
253+
console.log('ERROR eth balance', e);
254+
ethBalance = {
255+
balance: 0,
256+
verified: false,
257+
};
258+
}
259+
const multicall = new Multicall({
260+
web3Instance: this.web3,
261+
tryAggregate: true,
262+
multicallCustomContractAddress: '0xca11bde05977b3631167028862be2a173976ca11', // Getting multicall address from network fails
263+
});
264+
265+
const contractCallContext: ContractCallContext[] = erc20Contracts.map((erc20ContractAddress) => {
266+
return {
267+
reference: erc20ContractAddress,
268+
contractAddress: erc20ContractAddress,
269+
abi: ERC20ABI,
270+
calls: [
271+
{ reference: 'balanceOf', methodName: 'balanceOf', methodParameters: [address] },
272+
{ reference: 'decimals', methodName: 'decimals', methodParameters: [] },
273+
],
274+
};
275+
});
248276
const erc20Balances: Record<string, VerifiedBalance> = {};
249-
for (const erc20Contract of erc20Contracts) {
250-
const contract = new this.web3!.eth.Contract(balanceOfABI, erc20Contract.contractAddress, { dataInputFill: 'data' });
251-
console.log(contract.methods.balanceOf);
252-
let balance = await contract.methods.balanceOf().call();
253-
console.log(balance);
254-
// balance = parseFloat(formatUnits(hexToNumberString(contractBalance, erc20Contract.decimals)));
255-
erc20Balances[erc20Contract.contractAddress] = { balance: 0, verified: true };
277+
const results: ContractCallResults = await multicall.call(contractCallContext);
278+
279+
for (const key of Object.keys(results.results)) {
280+
const result = results.results[key];
281+
const erc20ContractAddress = key;
282+
console.log(erc20ContractAddress, result);
283+
if (result && result.callsReturnContext) {
284+
// @ts-ignore
285+
const balanceResult = result.callsReturnContext.find((call) => call.reference === 'balanceOf');
286+
// @ts-ignore
287+
const decimalsResult = result.callsReturnContext.find((call) => call.reference === 'decimals');
288+
289+
if (balanceResult?.success && decimalsResult?.success) {
290+
const decimals = decimalsResult?.returnValues[0];
291+
const balance = parseFloat(formatUnits(hexToNumberString(balanceResult?.returnValues[0].hex), decimals));
292+
console.log('contract', erc20ContractAddress, balance);
293+
erc20Balances[erc20ContractAddress] = { balance: balance, verified: true };
294+
} else {
295+
erc20Balances[erc20ContractAddress] = { balance: 0, verified: false };
296+
}
297+
}
256298
}
257299

300+
// const erc20Balances: Record<string, VerifiedBalance> = {};
301+
// for (const erc20ContractAddress of erc20Contracts) {
302+
// const contract = new this.web3!.eth.Contract(ERC20ABI, erc20ContractAddress, { dataInputFill: 'data' });
303+
// try {
304+
// // @ts-ignore
305+
// let balance = (await contract.methods.balanceOf(address).call()) as BigNumberish;
306+
// const decimals = (await contract.methods.decimals().call()) as Numeric;
307+
// console.log('contract', erc20ContractAddress, balance);
308+
// balance = parseFloat(formatUnits(balance, decimals));
309+
// erc20Balances[erc20ContractAddress] = { balance: balance, verified: true };
310+
// } catch (e) {
311+
// console.log('ERROR erc20 balance', e);
312+
// erc20Balances[erc20ContractAddress] = { balance: 0, verified: false };
313+
// }
314+
// }
315+
258316
return {
259317
balance: {
260-
ethBalance: {
261-
balance: Number(this.web3!.utils.fromWei(ethBalance, 'ether')),
262-
verified: true,
263-
},
318+
ethBalance,
264319
erc20Balances,
265320
},
266321
blockNumber: this.proofProvider?.getStatus().latest!,

src/common.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import erc20Contracts from '../public/erc20Contracts.json';
2-
31
import { LightClientVerifierInitArgs } from './LightClientVerifier';
42

53
export const Actions = {
@@ -15,8 +13,7 @@ export const initialConfig: LightClientVerifierInitArgs = {
1513
network: 'mainnet',
1614
beaconApiUrl: 'https://lodestar-mainnet.chainsafe.io',
1715
elRpcUrl: 'https://lodestar-mainnetrpc.chainsafe.io',
18-
initialCheckpoint: '0x8db70aac95f3a33616ab938e060fc7615b5e254634d13cd014a2c838fddc33a1',
19-
erc20Contracts,
16+
initialCheckpoint: '0x7fd9dccecb5fc37db1b9a12607795d4777635aa10ac774a07e871086a004c775',
2017
};
2118

2219
export const ETH = 'ETH';

0 commit comments

Comments
 (0)