Compare commits

...

6 Commits

Author SHA1 Message Date
semantic-release-bot 416c6a1b74 chore(release): 0.1.0-develop.1 [skip ci]
# [0.1.0-develop.1](https://git.lumeweb.com/LumeWeb/libethsync/compare/v0.0.1...v0.1.0-develop.1) (2023-07-11)

### Bug Fixes

* add missing repository to package.json ([127b1aa](127b1aa0d7))
* export createDefaultClient ([5d1bdec](5d1bdec620))

### Features

* Initial version ([24cd98b](24cd98bb3c))
* Initial version ([5843acb](5843acb79b))
2023-07-11 04:20:11 +00:00
Derrick Hammer 127b1aa0d7
fix: add missing repository to package.json 2023-07-11 00:19:07 -04:00
Derrick Hammer 5d1bdec620
fix: export createDefaultClient 2023-07-11 00:15:54 -04:00
Derrick Hammer 840dad7ad1
ci: setup 2023-07-10 16:49:47 -04:00
Derrick Hammer 24cd98bb3c
feat: Initial version 2023-07-10 16:48:01 -04:00
Derrick Hammer 5843acb79b
feat: Initial version 2023-07-10 16:47:50 -04:00
24 changed files with 24034 additions and 1 deletions

13
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Build/Publish
on:
push:
branches:
- master
- develop
- develop-*
jobs:
main:
uses: lumeweb/github-node-deploy-workflow/.github/workflows/main.yml@master
secrets: inherit

5
.presetterrc.json Normal file
View File

@ -0,0 +1,5 @@
{
"preset": [
"@lumeweb/node-library-preset"
]
}

13
CHANGELOG.md Normal file
View File

@ -0,0 +1,13 @@
# [0.1.0-develop.1](https://git.lumeweb.com/LumeWeb/libethsync/compare/v0.0.1...v0.1.0-develop.1) (2023-07-11)
### Bug Fixes
* add missing repository to package.json ([127b1aa](https://git.lumeweb.com/LumeWeb/libethsync/commit/127b1aa0d7f312ebfbc9ab1c88b595ecdc6b8e7a))
* export createDefaultClient ([5d1bdec](https://git.lumeweb.com/LumeWeb/libethsync/commit/5d1bdec620a0e077849606860634e935cdc2bd19))
### Features
* Initial version ([24cd98b](https://git.lumeweb.com/LumeWeb/libethsync/commit/24cd98bb3ccb888400fe9e205fc45606c934f879))
* Initial version ([5843acb](https://git.lumeweb.com/LumeWeb/libethsync/commit/5843acb79bacca113cf08c9fd64a3edb6f97dc5c))

View File

@ -1,2 +1,2 @@
# libethclient
# libethsync

21601
npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load Diff

60
package.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "@lumeweb/libethsync",
"version": "0.1.0-develop.1",
"type": "module",
"repository": {
"type": "git",
"url": "gitea@git.lumeweb.com:LumeWeb/libethsync.git"
},
"devDependencies": {
"@lumeweb/node-library-preset": "^0.2.7",
"presetter": "*"
},
"readme": "ERROR: No README data found!",
"scripts": {
"prepare": "presetter bootstrap",
"build": "run build",
"semantic-release": "semantic-release"
},
"exports": {
"./client": {
"import": "./lib/client/index.js",
"typings": "./lib/client/index.d.ts"
},
"./node": {
"import": "./lib/node/index.js",
"typings": "./lib/node/index.d.ts"
}
},
"dependencies": {
"@chainsafe/as-sha256": "^0.3.1",
"@chainsafe/bls": "7.1.1",
"@chainsafe/blst": "0.2.9",
"@chainsafe/ssz": "0.11.1",
"@ethereumjs/block": "4.3.0",
"@ethereumjs/blockchain": "6.3.0",
"@ethereumjs/common": "^3.2.0",
"@ethereumjs/trie": "5.1.0",
"@ethereumjs/tx": "4.2.0",
"@ethereumjs/util": "8.1.0",
"@ethereumjs/vm": "6.5.0",
"@lodestar/config": "1.9.1",
"@lodestar/light-client": "1.9.1",
"@lodestar/params": "1.9.1",
"@lodestar/types": "1.9.1",
"async-mutex": "^0.4.0",
"axios": "^1.4.0",
"axios-retry": "^3.5.1",
"detect-node": "^2.1.0",
"ethereum-cryptography": "^2.0.0",
"node-cache": "^5.1.2",
"rlp": "^3.0.0",
"web3-types": "^1.0.1"
},
"files": [
"lib"
],
"publishConfig": {
"access": "public"
}
}

97
src/baseClient.ts Normal file
View File

@ -0,0 +1,97 @@
import { ClientConfig, ExecutionInfo, IProver, IStore } from "#interfaces.js";
import { POLLING_DELAY } from "#constants.js";
import {
computeSyncPeriodAtSlot,
getCurrentSlot,
} from "@lodestar/light-client/utils";
import { init } from "@chainsafe/bls/switchable";
import { Mutex } from "async-mutex";
import { fromHexString } from "@chainsafe/ssz";
import { getDefaultClientConfig } from "#util.js";
import isNode from "detect-node";
export interface BaseClientOptions {
prover: IProver;
store?: IStore;
}
export default abstract class BaseClient {
protected latestCommittee?: Uint8Array[];
protected latestBlockHash?: string;
protected config: ClientConfig = getDefaultClientConfig();
protected genesisCommittee: Uint8Array[] = this.config.genesis.committee.map(
(pk) => fromHexString(pk),
);
protected genesisPeriod = computeSyncPeriodAtSlot(this.config.genesis.slot);
private genesisTime = this.config.genesis.time;
protected booted = false;
private syncMutex = new Mutex();
protected options: BaseClientOptions;
protected;
constructor(options: BaseClientOptions) {
this.options = options;
}
private _latestPeriod: number = -1;
get latestPeriod(): number {
return this._latestPeriod;
}
public get isSynced() {
return this._latestPeriod === this.getCurrentPeriod();
}
public async sync(): Promise<void> {
await init(isNode ? "blst-native" : "herumi");
await this._sync();
}
public getCurrentPeriod(): number {
return computeSyncPeriodAtSlot(
getCurrentSlot(this.config.chainConfig, this.genesisTime),
);
}
public async getNextValidExecutionInfo(
retry: number = 10,
): Promise<ExecutionInfo> {
if (retry === 0)
throw new Error(
"no valid execution payload found in the given retry limit",
);
const ei = await this.getLatestExecution();
if (ei) return ei;
// delay for the next slot
await new Promise((resolve) => setTimeout(resolve, POLLING_DELAY));
return this.getNextValidExecutionInfo(retry - 1);
}
protected async _sync() {
await this.syncMutex.acquire();
const currentPeriod = this.getCurrentPeriod();
if (currentPeriod > this._latestPeriod) {
if (!this.booted) {
this.latestCommittee = await this.syncFromGenesis();
} else {
this.latestCommittee = await this.syncFromLastUpdate();
}
this._latestPeriod = currentPeriod;
}
this.syncMutex.release();
}
// committee and prover index of the first honest prover
protected abstract syncFromGenesis(): Promise<Uint8Array[]>;
protected abstract syncFromLastUpdate(
startPeriod?: number,
): Promise<Uint8Array[]>;
protected abstract getLatestExecution(): Promise<ExecutionInfo | null>;
}

137
src/client/client.ts Normal file
View File

@ -0,0 +1,137 @@
import BaseClient, { BaseClientOptions } from "#baseClient.js";
import {
ExecutionInfo,
IStore,
IVerifyingProvider,
IVerifyingProviderConstructor,
} from "#interfaces.js";
import { DEFAULT_BATCH_SIZE, POLLING_DELAY } from "#constants.js";
import { IClientProver } from "#client/prover.js";
import {
getCommitteeHash,
optimisticUpdateFromJSON,
optimisticUpdateVerify,
} from "#util.js";
import { equalBytes } from "@noble/curves/abstract/utils.js";
interface Config extends BaseClientOptions {
prover: IClientProver;
beaconUrl: string;
provider: IVerifyingProviderConstructor;
rpcHandler: Function;
}
export default class Client extends BaseClient {
private provider?: IVerifyingProvider;
protected declare options: Config;
constructor(options: Config) {
super(options);
}
protected async getLatestExecution(): Promise<ExecutionInfo | null> {
const updateJSON = await this.options.prover.callback(
"consensus_optimistic_update",
);
const update = optimisticUpdateFromJSON(updateJSON);
const verify = await optimisticUpdateVerify(
this.latestCommittee as Uint8Array[],
update,
);
if (!verify.correct) {
console.error(`Invalid Optimistic Update: ${verify.reason}`);
return null;
}
console.log(
`Optimistic update verified for slot ${updateJSON.attested_header.beacon.slot}`,
);
return {
blockHash: updateJSON.attested_header.execution.block_hash,
blockNumber: updateJSON.attested_header.execution.block_number,
};
}
async sync(): Promise<void> {
await super.sync();
if (!this.provider) {
const { blockHash, blockNumber } = await this.getNextValidExecutionInfo();
const factory = this.options.provider;
const provider = new factory(
this.options.rpcHandler,
blockNumber,
blockHash,
);
this.subscribe((ei) => {
console.log(
`Received a new blockheader: ${ei.blockNumber} ${ei.blockHash}`,
);
provider.update(ei.blockNumber, ei.blockHash);
});
this.provider = provider;
this.booted = true;
}
}
protected syncFromGenesis(): Promise<Uint8Array[]> {
return Promise.resolve([]);
}
protected async syncFromLastUpdate(
startPeriod = this.latestPeriod,
): Promise<Uint8Array[]> {
const currentPeriod = this.getCurrentPeriod();
let lastCommitteeHash: Uint8Array = getCommitteeHash(this.genesisCommittee);
for (let period = startPeriod + 1; period <= currentPeriod; period++) {
try {
lastCommitteeHash = await this.options.prover.getCommitteeHash(
period,
currentPeriod,
DEFAULT_BATCH_SIZE,
);
} catch (e: any) {
throw new Error(
`failed to fetch committee hash for prover at period(${period}): ${e.meessage}`,
);
}
}
return this.getCommittee(currentPeriod, lastCommitteeHash);
}
private async subscribe(callback: (ei: ExecutionInfo) => void) {
setInterval(async () => {
try {
await this._sync();
const ei = await this.getLatestExecution();
if (ei && ei.blockHash !== this.latestBlockHash) {
this.latestBlockHash = ei.blockHash;
return await callback(ei);
}
} catch (e) {
console.error(e);
}
}, POLLING_DELAY);
}
private async getCommittee(
period: number,
expectedCommitteeHash: Uint8Array | null,
): Promise<Uint8Array[]> {
if (period === this.genesisPeriod) {
return this.genesisCommittee;
}
if (!expectedCommitteeHash) {
throw new Error("expectedCommitteeHash required");
}
const committee = await this.options.prover.getCommittee(period);
const committeeHash = getCommitteeHash(committee);
if (!equalBytes(committeeHash, expectedCommitteeHash as Uint8Array)) {
throw new Error("prover responded with an incorrect committee");
}
return committee;
}
}

19
src/client/index.ts Normal file
View File

@ -0,0 +1,19 @@
import Client from "./client.js";
import Prover, { IClientProver, ProverRequestCallback } from "./prover.js";
import VerifyingProvider from "./verifyingProvider.js";
function createDefaultClient(
beaconUrl: string,
proverHandler: ProverRequestCallback,
rpcHandler: Function,
): Client {
return new Client({
prover: new Prover(proverHandler),
beaconUrl,
provider: VerifyingProvider,
rpcHandler,
});
}
export { Client, Prover, VerifyingProvider, createDefaultClient };
export { IClientProver, ProverRequestCallback };

73
src/client/prover.ts Normal file
View File

@ -0,0 +1,73 @@
import {
ConsensusCommitteeHashesRequest,
ConsensusCommitteePeriodRequest,
IProver,
} from "#interfaces.js";
import { CommitteeSSZ, HashesSSZ } from "#ssz.js";
import { LightClientUpdate } from "#types.js";
import * as capella from "@lodestar/types/capella";
export type ProverRequestCallback = (
action: string,
args?: ConsensusCommitteeHashesRequest | ConsensusCommitteePeriodRequest,
) => any;
export interface IClientProver extends IProver {
get callback(): ProverRequestCallback;
getCommittee(period: number | "latest"): Promise<Uint8Array[]>;
getSyncUpdate(period: number): Promise<LightClientUpdate>;
getCommitteeHash(
period: number,
currentPeriod: number,
cacheCount: number,
): Promise<Uint8Array>;
}
export default class Prover implements IClientProver {
cachedHashes: Map<number, Uint8Array> = new Map();
constructor(callback: ProverRequestCallback) {
this._callback = callback;
}
private _callback: ProverRequestCallback;
get callback(): ProverRequestCallback {
return this._callback;
}
async getCommittee(period: number | "latest"): Promise<Uint8Array[]> {
const res = await this.callback("consensus_committee_period", { period });
return CommitteeSSZ.deserialize(Uint8Array.from(Object.values(res)));
}
async getSyncUpdate(period: number): Promise<LightClientUpdate> {
const res = await this.callback("consensus_committee_period", { period });
return capella.ssz.LightClientUpdate.deserialize(
Uint8Array.from(Object.values(res)),
);
}
async _getHashes(startPeriod: number, count: number): Promise<Uint8Array[]> {
const res = await this.callback("consensus_committee_hashes", {
start: startPeriod,
count,
});
return HashesSSZ.deserialize(Uint8Array.from(Object.values(res)));
}
async getCommitteeHash(
period: number,
currentPeriod: number,
cacheCount: number,
): Promise<Uint8Array> {
const _count = Math.min(currentPeriod - period + 1, cacheCount);
if (!this.cachedHashes.has(period)) {
const vals = await this._getHashes(period, _count);
for (let i = 0; i < _count; i++) {
this.cachedHashes.set(period + i, vals[i]);
}
}
return this.cachedHashes.get(period)!;
}
}

81
src/client/rpc.ts Normal file
View File

@ -0,0 +1,81 @@
export type RPCRequest = {
method: string;
params: any[];
};
export type RPCRequestRaw = RPCRequest & {
jsonrpc: string;
id: string;
};
export type RPCResponse = {
success: boolean;
result: any;
};
export class RPC {
private callback: Function;
constructor(callback: Function) {
this.callback = callback;
}
async request(request: RPCRequest) {
return await this._retryRequest(request);
}
async requestBatch(requests: RPCRequest[]) {
const res: RPCResponse[] = [];
for (const request of requests) {
const r = await this._retryRequest(request);
res.push(r);
}
return res;
}
private async _retryRequest(
_request: RPCRequest,
retry = 5,
): Promise<RPCResponse> {
const request = {
..._request,
jsonrpc: "2.0",
id: this.generateId(),
};
for (let i = retry; i > 0; i--) {
const res = await this._request(request);
if (res.success) {
return res;
} else if (i == 1) {
console.error(
`RPC batch request failed after maximum retries: ${JSON.stringify(
request,
null,
2,
)} ${JSON.stringify(res, null, 2)}`,
);
}
}
throw new Error("RPC request failed");
}
private generateId(): string {
return Math.floor(Math.random() * 2 ** 64).toFixed();
}
protected async _request(request: RPCRequestRaw): Promise<RPCResponse> {
try {
const response = await this.callback(request);
return {
success: !response.error,
result: response.error || response.result,
};
} catch (e) {
return {
success: false,
result: { message: `request failed: ${e}` },
};
}
}
}

65
src/client/types.ts Normal file
View File

@ -0,0 +1,65 @@
import { Bytes32 } from "#types.js";
export type AddressHex = string;
export type HexString = string;
export type Bytes = string;
export type AccountResponse = GetProof;
export type CodeResponse = string;
export type AccessList = { address: AddressHex; storageKeys: Bytes32[] }[];
export interface RPCTx {
from?: string;
to?: string;
gas?: string;
gasPrice?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
accessList?: AccessList;
value?: string;
data?: string;
}
export type JSONRPCReceipt = {
transactionHash: string;
transactionIndex: string;
blockHash: string;
blockNumber: string;
from: string;
to: string | null;
cumulativeGasUsed: string;
effectiveGasPrice: string;
gasUsed: string;
contractAddress: string | null;
logs: JSONRPCLog[];
logsBloom: string;
root?: string;
status?: string;
};
export type JSONRPCLog = {
removed: boolean;
logIndex: string | null;
transactionIndex: string | null;
transactionHash: string | null;
blockHash: string | null;
blockNumber: string | null;
address: string;
data: string;
topics: string[];
};
export interface GetProof {
address: string;
balance: string;
codeHash: string;
nonce: string;
storageHash: string;
accountProof: string[];
storageProof: StorageProof[];
}
export interface StorageProof {
key: string;
value: string;
proof: string[];
}

70
src/client/utils.ts Normal file
View File

@ -0,0 +1,70 @@
import {
Account,
bigIntToHex,
setLengthLeft,
toBuffer,
} from "@ethereumjs/util";
import { BlockData, HeaderData } from "@ethereumjs/block";
import {
AccessListEIP2930TxData,
FeeMarketEIP1559TxData,
TxData,
} from "@ethereumjs/tx";
const isTruthy = (val: any) => !!val;
// TODO: fix blockInfo type
export function headerDataFromWeb3Response(blockInfo: any): HeaderData {
return {
parentHash: blockInfo.parentHash,
uncleHash: blockInfo.sha3Uncles,
coinbase: blockInfo.miner,
stateRoot: blockInfo.stateRoot,
transactionsTrie: blockInfo.transactionsRoot,
receiptTrie: blockInfo.receiptsRoot,
logsBloom: blockInfo.logsBloom,
difficulty: BigInt(blockInfo.difficulty),
number: BigInt(blockInfo.number),
gasLimit: BigInt(blockInfo.gasLimit),
gasUsed: BigInt(blockInfo.gasUsed),
timestamp: BigInt(blockInfo.timestamp),
extraData: blockInfo.extraData,
mixHash: (blockInfo as any).mixHash, // some reason the types are not up to date :(
nonce: blockInfo.nonce,
baseFeePerGas: blockInfo.baseFeePerGas
? BigInt(blockInfo.baseFeePerGas)
: undefined,
};
}
export function txDataFromWeb3Response(
txInfo: any,
): TxData | AccessListEIP2930TxData | FeeMarketEIP1559TxData {
return {
...txInfo,
data: txInfo.input,
gasPrice: BigInt(txInfo.gasPrice),
gasLimit: txInfo.gas,
to: isTruthy(txInfo.to)
? setLengthLeft(toBuffer(txInfo.to), 20)
: undefined,
value: BigInt(txInfo.value),
maxFeePerGas: isTruthy(txInfo.maxFeePerGas)
? BigInt(txInfo.maxFeePerGas)
: undefined,
maxPriorityFeePerGas: isTruthy(txInfo.maxPriorityFeePerGas)
? BigInt(txInfo.maxPriorityFeePerGas)
: undefined,
};
}
export function blockDataFromWeb3Response(blockInfo: any): BlockData {
return {
header: headerDataFromWeb3Response(blockInfo),
transactions: blockInfo.transactions.map(txDataFromWeb3Response),
};
}
export { bigIntToHex };
export const emptyAccountSerialize = new Account().serialize();

View File

@ -0,0 +1,716 @@
import { IVerifyingProvider } from "#interfaces.js";
import _ from "lodash";
import { Trie } from "@ethereumjs/trie";
import rlp from "rlp";
import { Chain, Common } from "@ethereumjs/common";
import {
Account,
Address,
bufferToHex,
KECCAK256_NULL_S,
setLengthLeft,
toBuffer,
toType,
TypeOutput,
} from "@ethereumjs/util";
import { VM } from "@ethereumjs/vm";
import { Blockchain } from "@ethereumjs/blockchain";
import { TransactionFactory } from "@ethereumjs/tx";
import { Block, BlockHeader } from "@ethereumjs/block";
import { Bytes32 } from "#types.js";
import type { BlockNumberOrTag } from "web3-types";
import {
DEFAULT_BLOCK_PARAMETER,
MAX_BLOCK_FUTURE,
MAX_BLOCK_HISTORY,
ZERO_ADDR,
} from "#constants.js";
import {
bigIntToHex,
blockDataFromWeb3Response,
emptyAccountSerialize,
headerDataFromWeb3Response,
} from "./utils.js";
import {
AccessList,
AccountResponse,
AddressHex,
Bytes,
CodeResponse,
GetProof,
HexString,
JSONRPCReceipt,
RPCTx,
} from "./types.js";
import { keccak256 } from "ethereum-cryptography/keccak";
import { fromHexString } from "@chainsafe/ssz";
import { RPC } from "#client/rpc.js";
export default class VerifyingProvider implements IVerifyingProvider {
common: Common;
vm: VM | null = null;
private blockHashes: { [blockNumberHex: string]: Bytes32 } = {};
private blockPromises: {
[blockNumberHex: string]: { promise: Promise<void>; resolve: () => void };
} = {};
private blockHeaders: { [blockHash: string]: BlockHeader } = {};
private latestBlockNumber: number;
private _methods: Map<string, Function> = new Map<string, Function>(
Object.entries({
eth_getBalance: this.getBalance,
eth_blockNumber: this.blockNumber,
eth_chainId: this.chainId,
eth_getCode: this.getCode,
eth_getTransactionCount: this.getTransactionCount,
eth_call: this.call,
eth_estimateGas: this.estimateGas,
eth_sendRawTransaction: this.sendRawTransaction,
eth_getTransactionReceipt: this.getTransactionReceipt,
}),
);
private rpc: RPC;
constructor(
rpcHandler: Function,
blockNumber: number,
blockHash: Bytes32,
chain: bigint | Chain = Chain.Mainnet,
) {
this.common = new Common({
chain,
});
const _blockNumber = BigInt(blockNumber);
this.latestBlockNumber = blockNumber;
this.blockHashes[bigIntToHex(_blockNumber)] = blockHash;
this.rpc = new RPC(rpcHandler);
}
update(blockNumber: number, blockHash: Bytes32) {
const blockNumberHex = bigIntToHex(BigInt(blockNumber));
if (
blockNumberHex in this.blockHashes &&
this.blockHashes[blockNumberHex] !== blockHash
) {
console.log(
"Overriding an existing verified blockhash. Possibly the chain had a reorg",
);
}
const latestBlockNumber = this.latestBlockNumber;
this.latestBlockNumber = blockNumber;
this.blockHashes[blockNumberHex] = blockHash;
if (blockNumber > latestBlockNumber) {
for (
let b = BigInt(latestBlockNumber) + BigInt(1);
b <= blockNumber;
b++
) {
const bHex = bigIntToHex(b);
if (bHex in this.blockPromises) {
this.blockPromises[bHex].resolve();
}
}
}
}
public async rpcMethod(method: string, params: any) {
if (this._methods.has(method)) {
return this._methods.get(method)?.bind(this)(...params);
}
throw new Error("method not found");
}
private async getBalance(
addressHex: AddressHex,
blockOpt: BlockNumberOrTag = DEFAULT_BLOCK_PARAMETER,
) {
const header = await this.getBlockHeader(blockOpt);
const address = Address.fromString(addressHex);
const { result: proof, success } = await this.rpc.request({
method: "eth_getProof",
params: [addressHex, [], bigIntToHex(header.number)],
});
if (!success) {
throw new Error(`RPC request failed`);
}
const isAccountCorrect = await this.verifyProof(
address,
[],
header.stateRoot,
proof,
);
if (!isAccountCorrect) {
throw new Error("Invalid account proof provided by the RPC");
}
return bigIntToHex(proof.balance);
}
private async blockNumber(): Promise<HexString> {
return bigIntToHex(BigInt(this.latestBlockNumber));
}
private async chainId(): Promise<HexString> {
return bigIntToHex(this.common.chainId());
}
private async getCode(
addressHex: AddressHex,
blockOpt: BlockNumberOrTag = DEFAULT_BLOCK_PARAMETER,
): Promise<HexString> {
const header = await this.getBlockHeader(blockOpt);
const res = await this.rpc.requestBatch([
{
method: "eth_getProof",
params: [addressHex, [], bigIntToHex(header.number)],
},
{
method: "eth_getCode",
params: [addressHex, bigIntToHex(header.number)],
},
]);
if (res.some((r) => !r.success)) {
throw new Error(`RPC request failed`);
}
const [accountProof, code] = [res[0].result, res[1].result];
const address = Address.fromString(addressHex);
const isAccountCorrect = await this.verifyProof(
address,
[],
header.stateRoot,
accountProof,
);
if (!isAccountCorrect) {
throw new Error(`invalid account proof provided by the RPC`);
}
const isCodeCorrect = await this.verifyCodeHash(
code,
accountProof.codeHash,
);
if (!isCodeCorrect) {
throw new Error(
`code provided by the RPC doesn't match the account's codeHash`,
);
}
return code;
}
private async getTransactionCount(
addressHex: AddressHex,
blockOpt: BlockNumberOrTag = DEFAULT_BLOCK_PARAMETER,
): Promise<HexString> {
const header = await this.getBlockHeader(blockOpt);
const address = Address.fromString(addressHex);
const { result: proof, success } = await this.rpc.request({
method: "eth_getProof",
params: [addressHex, [], bigIntToHex(header.number)],
});
if (!success) {
throw new Error(`RPC request failed`);
}
const isAccountCorrect = await this.verifyProof(
address,
[],
header.stateRoot,
proof,
);
if (!isAccountCorrect) {
throw new Error(`invalid account proof provided by the RPC`);
}
return bigIntToHex(proof.nonce.toString());
}
private async call(
transaction: RPCTx,
blockOpt: BlockNumberOrTag = DEFAULT_BLOCK_PARAMETER,
) {
try {
this.validateTx(transaction);
} catch (e: any) {
throw new Error((e as Error).message);
}
const header = await this.getBlockHeader(blockOpt);
const vm = await this.getVM(transaction, header);
const {
from,
to,
gas: gasLimit,
gasPrice,
maxPriorityFeePerGas,
value,
data,
} = transaction;
try {
const runCallOpts = {
caller: from ? Address.fromString(from) : undefined,
to: to ? Address.fromString(to) : undefined,
gasLimit: toType(gasLimit, TypeOutput.BigInt),
gasPrice: toType(gasPrice || maxPriorityFeePerGas, TypeOutput.BigInt),
value: toType(value, TypeOutput.BigInt),
data: data ? toBuffer(data) : undefined,
block: { header },
};
const { execResult } = await vm.evm.runCall(runCallOpts);
return bufferToHex(execResult.returnValue);
} catch (error: any) {
throw new Error(error.message.toString());
}
}
private async estimateGas(
transaction: RPCTx,
blockOpt: BlockNumberOrTag = DEFAULT_BLOCK_PARAMETER,
) {
try {
this.validateTx(transaction);
} catch (e) {
throw new Error((e as Error).message);
}
const header = await this.getBlockHeader(blockOpt);
if (transaction.gas == undefined) {
// If no gas limit is specified use the last block gas limit as an upper bound.
transaction.gas = bigIntToHex(header.gasLimit);
}
const txType = BigInt(
transaction.maxFeePerGas || transaction.maxPriorityFeePerGas
? 2
: transaction.accessList
? 1
: 0,
);
if (txType == BigInt(2)) {
transaction.maxFeePerGas =
transaction.maxFeePerGas || bigIntToHex(header.baseFeePerGas!);
} else {
if (
transaction.gasPrice == undefined ||
BigInt(transaction.gasPrice) === BigInt(0)
) {
transaction.gasPrice = bigIntToHex(header.baseFeePerGas!);
}
}
const txData = {
...transaction,
type: bigIntToHex(txType),
gasLimit: transaction.gas,
};
const tx = TransactionFactory.fromTxData(txData, {
common: this.common,
freeze: false,
});
const vm = await this.getVM(transaction, header);
// set from address
const from = transaction.from
? Address.fromString(transaction.from)
: Address.zero();
tx.getSenderAddress = () => {
return from;
};
try {
const { totalGasSpent } = await vm.runTx({
tx,
skipNonce: true,
skipBalance: true,
skipBlockGasLimitValidation: true,
block: { header } as any,
});
return bigIntToHex(totalGasSpent);
} catch (error: any) {
throw new Error(error.message.toString());
}
}
private async sendRawTransaction(signedTx: string): Promise<string> {
// TODO: brodcast tx directly to the mem pool?
const { success } = await this.rpc.request({
method: "eth_sendRawTransaction",
params: [signedTx],
});
if (!success) {
throw new Error(`RPC request failed`);
}
const tx = TransactionFactory.fromSerializedData(toBuffer(signedTx), {
common: this.common,
});
return bufferToHex(tx.hash());
}
private async getTransactionReceipt(
txHash: Bytes32,
): Promise<JSONRPCReceipt | null> {
const { result: receipt, success } = await this.rpc.request({
method: "eth_getTransactionReceipt",
params: [txHash],
});
if (!(success && receipt)) {
return null;
}
const header = await this.getBlockHeader(receipt.blockNumber);
const block = await this.getBlock(header);
const index = block.transactions.findIndex(
(tx) => bufferToHex(tx.hash()) === txHash.toLowerCase(),
);
if (index === -1) {
throw new Error("the recipt provided by the RPC is invalid");
}
const tx = block.transactions[index];
return {
transactionHash: txHash,
transactionIndex: bigIntToHex(BigInt(index)),
blockHash: bufferToHex(block.hash()),
blockNumber: bigIntToHex(block.header.number),
from: tx.getSenderAddress().toString(),
to: tx.to?.toString() ?? null,
cumulativeGasUsed: "0x0",
effectiveGasPrice: "0x0",
gasUsed: "0x0",
contractAddress: null,
logs: [],
logsBloom: "0x0",
status: BigInt(receipt.status) ? "0x1" : "0x0", // unverified!!
};
}
private async getVMCopy(): Promise<VM> {
if (this.vm === null) {
const blockchain = await Blockchain.create({ common: this.common });
// path the blockchain to return the correct blockhash
(blockchain as any).getBlock = async (blockId: number) => {
const _hash = toBuffer(await this.getBlockHash(BigInt(blockId)));
return {
hash: () => _hash,
};
};
this.vm = await VM.create({ common: this.common, blockchain });
}
return await this.vm!.copy();
}
private async getVM(tx: RPCTx, header: BlockHeader): Promise<VM> {
// forcefully set gasPrice to 0 to avoid not enough balance error
const _tx = {
to: tx.to,
from: tx.from ? tx.from : ZERO_ADDR,
data: tx.data,
value: tx.value,
gasPrice: "0x0",
gas: tx.gas ? tx.gas : bigIntToHex(header.gasLimit!),
};
const { result, success } = await this.rpc.request({
method: "eth_createAccessList",
params: [_tx, bigIntToHex(header.number)],
});
if (!success) {
throw new Error(`RPC request failed`);
}
const accessList = result.accessList as AccessList;
accessList.push({ address: _tx.from, storageKeys: [] });
if (_tx.to && !accessList.some((a) => a.address.toLowerCase() === _tx.to)) {
accessList.push({ address: _tx.to, storageKeys: [] });
}
const vm = await this.getVMCopy();
await vm.stateManager.checkpoint();
const requests = accessList
.map((access) => {
return [
{
method: "eth_getProof",
params: [
access.address,
access.storageKeys,
bigIntToHex(header.number),
],
},
{
method: "eth_getCode",
params: [access.address, bigIntToHex(header.number)],
},
];
})
.flat();
const rawResponse = await this.rpc.requestBatch(requests);
if (rawResponse.some((r: any) => !r.success)) {
throw new Error(`RPC request failed`);
}
const responses = rawResponse
.map((r: any) => r.result)
.reduce(
(acc, curr, idx, arr) =>
idx % 2 === 0 ? acc.push([curr, arr[idx + 1]]) : acc,
[],
) as [AccountResponse, CodeResponse][];
for (let i = 0; i < accessList.length; i++) {
const { address: addressHex, storageKeys } = accessList[i];
const [accountProof, code] = responses[i];
const {
nonce,
balance,
codeHash,
storageProof: storageAccesses,
} = accountProof;
const address = Address.fromString(addressHex);
const isAccountCorrect = await this.verifyProof(
address,
storageKeys,
header.stateRoot,
accountProof,
);
if (!isAccountCorrect) {
throw new Error(`invalid account proof provided by the RPC`);
}
const isCodeCorrect = await this.verifyCodeHash(code, codeHash);
if (!isCodeCorrect) {
throw new Error(
`code provided by the RPC doesn't match the account's codeHash`,
);
}
const account = Account.fromAccountData({
nonce: BigInt(nonce),
balance: BigInt(balance),
codeHash,
});
await vm.stateManager.putAccount(address, account);
for (let storageAccess of storageAccesses) {
await vm.stateManager.putContractStorage(
address,
setLengthLeft(toBuffer(storageAccess.key), 32),
setLengthLeft(toBuffer(storageAccess.value), 32),
);
}
if (code !== "0x")
await vm.stateManager.putContractCode(address, toBuffer(code));
}
await vm.stateManager.commit();
return vm;
}
private async getBlockHeader(
blockOpt: BlockNumberOrTag,
): Promise<BlockHeader> {
const blockNumber = this.getBlockNumberByBlockNumberOrTag(blockOpt);
await this.waitForBlockNumber(blockNumber);
const blockHash = await this.getBlockHash(blockNumber);
return this.getBlockHeaderByHash(blockHash);
}
private getBlockNumberByBlockNumberOrTag(blockOpt: BlockNumberOrTag): bigint {
// TODO: add support for blockOpts below
if (
typeof blockOpt === "string" &&
["pending", "earliest", "finalized", "safe"].includes(blockOpt)
) {
throw new Error(`"pending" is not yet supported`);
} else if (blockOpt === "latest") {
return BigInt(this.latestBlockNumber);
} else {
const blockNumber = BigInt(blockOpt as any);
if (blockNumber > BigInt(this.latestBlockNumber) + MAX_BLOCK_FUTURE) {
throw new Error("specified block is too far in future");
} else if (blockNumber + MAX_BLOCK_HISTORY < this.latestBlockNumber) {
throw new Error(
`specified block cannot older that ${MAX_BLOCK_HISTORY}`,
);
}
return blockNumber;
}
}
private async waitForBlockNumber(blockNumber: bigint) {
if (blockNumber <= this.latestBlockNumber) return;
console.log(`waiting for blockNumber ${blockNumber}`);
const blockNumberHex = bigIntToHex(blockNumber);
if (!(blockNumberHex in this.blockPromises)) {
let r: () => void = () => {};
const p = new Promise<void>((resolve) => {
r = resolve;
});
this.blockPromises[blockNumberHex] = {
promise: p,
resolve: r,
};
}
return this.blockPromises[blockNumberHex].promise;
}
private async getBlockHeaderByHash(blockHash: Bytes32) {
if (!this.blockHeaders[blockHash]) {
const { result: blockInfo, success } = await this.rpc.request({
method: "eth_getBlockByHash",
params: [blockHash, true],
});
if (!success) {
throw new Error(`RPC request failed`);
}
const headerData = headerDataFromWeb3Response(blockInfo);
const header = BlockHeader.fromHeaderData(headerData);
if (!header.hash().equals(toBuffer(blockHash))) {
throw new Error(
`blockhash doesn't match the blockInfo provided by the RPC`,
);
}
this.blockHeaders[blockHash] = header;
}
return this.blockHeaders[blockHash];
}
private async verifyProof(
address: Address,
storageKeys: Bytes32[],
stateRoot: Buffer,
proof: GetProof,
): Promise<boolean> {
const trie = new Trie();
const key = keccak256(address.toBuffer());
const expectedAccountRLP = await trie.verifyProof(
stateRoot,
toBuffer(key),
proof.accountProof.map((a) => toBuffer(a)),
);
const account = Account.fromAccountData({
nonce: BigInt(proof.nonce),
balance: BigInt(proof.balance),
storageRoot: proof.storageHash,
codeHash: proof.codeHash,
});
const isAccountValid = account
.serialize()
.equals(expectedAccountRLP ? expectedAccountRLP : emptyAccountSerialize);
if (!isAccountValid) {
return false;
}
if (storageKeys.length !== proof?.storageProof.length) {
console.error("missing storageProof");
throw new Error("missing storageProof");
}
for (let i = 0; i < storageKeys.length; i++) {
const sp = proof.storageProof[i];
const key = keccak256(setLengthLeft(toBuffer(storageKeys[i]), 32));
const expectedStorageRLP = await trie.verifyProof(
toBuffer(proof.storageHash),
toBuffer(key),
sp.proof.map((a) => toBuffer(a)),
);
const isStorageValid =
(!expectedStorageRLP && sp.value === "0x0") ||
(!!expectedStorageRLP &&
expectedStorageRLP.equals(Buffer.from(rlp.encode(sp.value))));
if (!isStorageValid) {
return false;
}
}
return true;
}
private verifyCodeHash(code: Bytes, codeHash: Bytes32): boolean {
return (
(code === "0x" && codeHash === "0x" + KECCAK256_NULL_S) ||
keccak256(fromHexString(codeHash)) === fromHexString(codeHash)
);
}
private validateTx(tx: RPCTx) {
if (tx.gasPrice !== undefined && tx.maxFeePerGas !== undefined) {
throw new Error("Cannot send both gasPrice and maxFeePerGas params");
}
if (tx.gasPrice !== undefined && tx.maxPriorityFeePerGas !== undefined) {
throw new Error("Cannot send both gasPrice and maxPriorityFeePerGas");
}
if (
tx.maxFeePerGas !== undefined &&
tx.maxPriorityFeePerGas !== undefined &&
BigInt(tx.maxPriorityFeePerGas) > BigInt(tx.maxFeePerGas)
) {
throw new Error(
`maxPriorityFeePerGas (${tx.maxPriorityFeePerGas.toString()}) is bigger than maxFeePerGas (${tx.maxFeePerGas.toString()})`,
);
}
}
private async getBlock(header: BlockHeader) {
const { result: blockInfo, success } = await this.rpc.request({
method: "eth_getBlockByNumber",
params: [bigIntToHex(header.number), true],
});
if (!success) {
throw new Error(`RPC request failed`);
}
// TODO: add support for uncle headers; First fetch all the uncles
// add it to the blockData, verify the uncles and use it
const blockData = blockDataFromWeb3Response(blockInfo);
const block = Block.fromBlockData(blockData, { common: this.common });
if (!block.header.hash().equals(header.hash())) {
throw new Error(
`BN(${header.number}): blockhash doest match the blockData provided by the RPC`,
);
}
if (!(await block.validateTransactionsTrie())) {
throw new Error(
`transactionTree doesn't match the transactions provided by the RPC`,
);
}
return block;
}
private async getBlockHash(blockNumber: bigint) {
if (blockNumber > this.latestBlockNumber)
throw new Error("cannot return blockhash for a blocknumber in future");
// TODO: fetch the blockHeader is batched request
let lastVerifiedBlockNumber = this.latestBlockNumber;
while (lastVerifiedBlockNumber > blockNumber) {
const hash =
this.blockHashes[bigIntToHex(BigInt(lastVerifiedBlockNumber))];
const header = await this.getBlockHeaderByHash(hash);
lastVerifiedBlockNumber--;
const parentBlockHash = bufferToHex(header.parentHash);
const parentBlockNumberHex = bigIntToHex(BigInt(lastVerifiedBlockNumber));
if (
parentBlockNumberHex in this.blockHashes &&
this.blockHashes[parentBlockNumberHex] !== parentBlockHash
) {
console.log(
"Overriding an existing verified blockhash. Possibly the chain had a reorg",
);
}
this.blockHashes[parentBlockNumberHex] = parentBlockHash;
}
return this.blockHashes[bigIntToHex(blockNumber)];
}
}

536
src/constants.ts Normal file
View File

@ -0,0 +1,536 @@
export const BEACON_SYNC_COMMITTEE_SIZE = 512;
export const mainnetConfig = {
genesis_time: "1606824023",
genesis_validator_root:
"0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95",
slot: "6275237",
committee_pk: [
"0x9163821b21398fabd7fc110483c7100d4561f84d34eb8fe702762f5ad15b21c1de05b7552e400750ef87107054d2024c",
"0xb5b3eed089c61c731ea4b8da89d79953d1dcd076f0388e5a9a83d06601ccf8daf0b853ca706f79fee60ed2cd9a385f24",
"0xa84a73bbc11ea45136d6b315ae470c6494c418f9ec66648f3345f73f886e2b79171f1dc8bd7ca678064a71976ddd6866",
"0x86de1398c34f7ef2a0d6ad10f8d09591c7d1fa1dc5336513bba619933c0bd48be956367261aad5d17553aa6052434b34",
"0xb97cb89ec7055b04c7b9fd7eccc4a45715395c7316e42c9e241eadaaf7744b82b70b78acd6c64f95dd7138978356cd6c",
"0x8ee3b246427b9a208b272db2613fdde603b40550e0f3205a6f005d5e38cb028bdba44803287a78bc9c577324ae9e5251",
"0xa29f0427fc761318e6122277937b066950a6d749a5795f26e80b9fe1d2b41accb26c344b241d39e354638892b7f617f4",
"0x91246b7f68034b436ba623979c9a665746f3de5de495115c042a2332f23578131ed0c61aa69c24a1b1c3ea0732dec40b",
"0x8dbe5cf5acd2e860969a29165e04c5e7126070526dc160e2a77ebdccb1921be81bc1cc8d6d3bf704ff5efa9e47e4c355",
"0xa372476af4b0896ffb47efe4725537f9d4a49e563058008c74d1397f5d0a66c934b2c29078bd8e89f4b055d817453f03",
"0x8deaccd6915a70536cf44352e36139ce41dcac10bdf28616cb61b0b3f2dfb819267255709969497c986602d46f24e49e",
"0x877c5114d0fd6b1e3fe5853d297c31768c25f5aaf4a56d21c0d1167497009c9360e323810fefa294768555509f1da41f",
"0xa909c87e08c44584aae53f9380652edbf2373a0f7042e57d4340b4353d932495dadc8f340048180e578d9f93be77ca74",
"0x84e194104f1fab523bb2e5a91367612b15bdcd4d736e7f4633df695e0c1aabedea01b3440915cf0790661eb2a47af9f4",
"0xb1b58b4efd14539178aa289ca9a1a3b57efc8b12e3fb94ce6ee0014d7ad30c5c93f4e652cb5773f9f7d375f3e01dce3b",
"0x92b2a5471d662a302c6b2b90f3eae2c055ac3eeaf29c90f1a41f43eb4564f0eb15a641077f83bf4b59b61dec0bd651d5",
"0xb83ad1c495bc838742bebf2b5a31f4abf201ec93565146b95417a6794ad9c9cb0b944d7945d35735995b248d0d30f687",
"0xb64c8327897cb8b16a02a180242ea8eb3a7e8053db0ee4bbdc83bbf00b87b2300f386e43caf4dfac7627844a441d1727",
"0x836eba6f2f08438b95cc2f9e8f45ec42fc843892def3005df9291de9ff0bb8a26442270c6acbad1ea009b1026dfdd09b",
"0x90fcf0aecdf3bae07054961cd0b13922a61215e2bc128d481cbaa76eb98dae426b5fc848d4c357b46aff6eabf766ba6b",
"0x8208bfbf689752bed2c110b19f66d90e336be2f93331f0e32f355652a258366f79843a0f528f17c893e6a7feaadc5a11",
"0xa1e099011217619de38f780b46ab4bb97c88968ba994e3e0b47c9c32fabc985f1f74fb40deafa6670d5fcb41add29c4d",
"0x8184b64e342d9c02e47461b494782247bb178613d6ba814b7e96dab1170675fb45d0a2dddd3cf119361c022ffb58f55e",
"0x939fddb7877e6064ad39888f06ee733bbfb8523adc72d381e5429592ecbfb16fd26cc7b009fcfa03c56390f2e5da7cda",
"0x98421b84b58fbf62c1017c0b252848ae09d91c461693834e8b016a3dc476fa95aa4673fe858422a1ddacc0f7c85ef4c5",
"0xa3f69eb68d11d8fdf2f3718b114a8f6deda97474e9986e879acd503c0cb4cb2de319a015ce28c10f161f4fa485354857",
"0x8c14681476836e1b86f9d6c52c56b48269d1ef359f6006eac5063b7831cc73ab5d30eaba8a1fa99edbe9da03e9755969",
"0xa4f1ecba907961d6a86aea821550b28f00551814188b0f5ddbbb486f3ac4d063554c48b4cd685b7104707f1b13dc1b2a",
"0x96d7b0775111dbbb747242bbf9735e441aa9775a58f915418dff8507a0990532256bf0dd03b8bab011d99ff6e2c35cd9",
"0x8c81dcaa669a0ce8fa46920385ed3abdcc8b8a3209a176c2d2aff1f3a1deee8f22e2b9477a33d58c1d91ec62eda1a931",
"0xae05610abffb2d21709f64b04d4b59e12a0d8a738fb8e9161705b0bbbf40a69a7f432850c82c4bfff96db7875fb69ced",
"0x8078a2aea4c2b52d93f2686caa0f128b18600c02b24725314b2d02e701ba0b60c8d1f168de8e4364b2cae31ecced4094",
"0xb6a4f4de40f1c39a3d99dda56fa3815d394e4585632e98c39873348053006099c317db09b2028e9d45644dbeab45a9fb",
"0x85950570508f1567455863018614ee8aaf58aff2e6d5f54c7ec3b8cad69257bc642355fbb652eea772b4e8f5ea6dfcb5",
"0xa8f5a8bf2ea05042da3a5e77e881e4002dceee5229839c2e19a78931a91fd162a8ca1b70493d21bb8c80dbc0d03ac10f",
"0x8e6e04cd6608d961a225c22b9d07420f545dba1855de35a2e541b15843c2fb9abc4165c61bca5f5ab8298870d99054ef",
"0x992de481659c968b23c03dcdd5a8ad0fa9d00ad54e6a348f754b6dfda5e133bea2c52c295059a6467e4d1e65636f8d0f",
"0x95c16a1ba548e4d7af503921a92d4af407e6da7cc383ce5153916b2dfb49ccd27fd513a1a0599b8b0a2bf75c0c3dff10",
"0xab7bf86088748ebd4a24eb8db91bc72351094e0203f3109194ff1506e239bf9102d128a37ecd911005e74ab7ee57397b",
"0xb375c65fddd939dd19563262fe63cda6f8eef858cce226c95023acff69a3748fdc15d6e2b51caeb6adc60e36cb741ef6",
"0x8cacf8dfc5ca961a13ab4adc6a21e2c8d81a575d70eb2007bf6d78d8e01b032b2774e978855d226d8fceb2c57b5cd69a",
"0x9279d1e437728bbb0c9f79b534a5c394e08c48b5f39fdbc5f4743a36f5b58717bcd0def9c30170f4e2ce761137645f14",
"0xb2c5e1fb714cd67aef7a4b35d379c13f5341236cf66b17aeccebe84d250d1b5ace9605b767b1d1abad7d226be50b003b",
"0x805a7629f90f4a8efd744248741c128b02369f3e680ab3029164faf47bb2bfc5fe02f02f4bc3c76ad83b3397335d19c3",
"0xae31a38e151d69ce1c95f26a92472c3c29a8574c438cc2766a7da55bc563fb92f190c4a2322080f3344c504c0aee1556",
"0xa2c0bf5e88743903136a30ba8b49a635b85d112c2edd6770bbad3889cb6200632fe42f44bcdafcba905a4328e98ba3ba",
"0x8f0797a0ab508ac3dab337611afb42691919caf378bd9cce9cdfd0c4d158f20dc30ac33c2393a013c25873452b4320f4",
"0x907b31004301d30af194fb1951b01a954da2ee8b791b3c0eed31b623da59c0c7d6f13a8a906948d4b70c06356cf63892",
"0xa6ddc27d9b7e792a03eecf475f805106bfbf2fbfd0e1035e52926e9c6c5e111e5c5fcd6b93fc904320b0a770d9fa06ae",
"0x86ea128a4659dc60f32980d144cb23f1ad443049f343b8f06ff7d279c8afab03cc897e6d39dd4ed202edc559de2ea5ba",
"0xa847919d88e51fdd2c332938698154aadd5e7628f7a369a21600302b9756f7ec549e583b5d8f251fc0db6e917d4fdeda",
"0x8919698051604efa30f649fe84ec5ca0f893194a488f9b88d4e40f84a8bf79bb71433e3f91e7e4b63fdb3bc1b82c5e15",
"0x944752bea5b4ed9fde39739746571e3d33a42431774cb0b3f25e3b42bfd050f871d40f1f38674e4c8dc01b82a3c6ec60",
"0xb44651084265cb025249812a12d80bd4d4d187d341e63798a3471d3a790e3dcb65fd48feb94e677f346b1f71fea92d05",
"0x8da7a161a38d2b3679a9639ae6b33e06115ed92c20bad3dd9fe6cc17400dfb039cc7e96c764f00ed03635633b2947cb6",
"0x93ac2f91830afe49e824584a8633c90fcb33a107162788e709239e73879663a6a2a0777832d5374d5b7f2e2a84cd9a55",
"0x865733ae45cc093be1897cc5196f35b2dffb03d0a182550010bbce35d32489b0e4fdccbf74070c76e17a4465308e1f58",
"0xb900529b1f646d4f4f7dc3c314133553e27df54b6e46546e08e9977f5d4a171e59154c421976610560747e151edd5db6",
"0x9037840cba56ac61fe37ec3f58df287bd71cd73918fa43617bfe426715e118daaa21694beb2e8ad66bce407d4040e2a2",
"0x84f928611fc07144a10ed7fcd58f00e47fe9db2fceec778c9fad72dd68ce8d9e52aab9a8b62e7544a8d823108e10a87c",
"0x909dc44d972c837208e21aa96bb0a75d8e790afbad70f68a30893214fd0265e8d9a804e33ed851952fa52b6f1272bf52",
"0xb8f6f6371dd30318d1d6b0fde75d2eacb335eaa5bedba11137cee60aa0e533c4e6b56051d5265402f75e03fab98abb57",
"0x85543d8fafee7869b282fbc80e4022c8c1852ad83f8ddf363c86d72316fe356d0ca707c41d59ec46a736aecf54d1ce7b",
"0x86d0674250fe443f2aad96afa74a0b6f34c4aa62cf2053810e113c5184556ab9d8bb06f2080a0602ee2ea1bb3626a285",
"0xab5646a6e44a5ee2b2eb689d8090796795ef31cffa3e7262335f9180a93c104b7873d4829724b2c81dce812555e69622",
"0x8ca50b96907f6d0f5a1018d222e4ec6442fa47703ebfad8ecb01447e367d388ccb9292e8300aa0d7053cafbe923b3a31",
"0x8e6e81a3f7ce719acdb9aa84a56d4deff67497e1c355ba013173e05d760909eed1db0c2a4b9a2957925f21a564b71e19",
"0xa1f791e32fe47e794a5a25305e284b4d7aaaa6bdebb32421ac359968a2273412da334daaa8e25443748d88ac3a9d058b",
"0x9144aed94e9f3c8a6b9bc88ccdcd157c847261e382cca8ed3bbd3a896fcdb9146f00b9f8bb0f413aa7c9f3ddbe9af077",
"0x80a1d46002224cc82258626314616fa2b1a672212adee0e1be1ebd2f4feaaf6acb909b2fcf555c72867f83566df007ec",
"0xb1cb3dedf781792268c8c41eebd20fdf67211d3a9b6972637d677e58c3da6d8aaf5f163adb1bb779a3dc73dbeb4c3b8a",
"0xa547ae87c696c5f0b7679946de05ec0c55c1b2b00fb36855b05564c2d2d6c1df57b5696b309ba131b0097dfa7c264822",
"0xa24784af730f8c666df2b36d5d9a88f6c15e3cf605963fc84ca045b93b1df4360b0fdf74f977f06bc148a7d038c34c9e",
"0xb6e456c86204188f6e0f23493629a0df21180cbc8055ad69bdec0d31d948c4fb261ed891bdb5610eedae965aa5cb0234",
"0x8b9698a450cae8ea9c5b0245bd06ada77d98ee2de47c60bcce7b59cf7d8aa411c8ee1434e0d56959e56debfaab394451",
"0x86410dedbfa4e37aba412d876838fd96d83947cfb4f9aa1a6afa96ab2e1cc07714ec6255f0b824aac1581b14f0ef99dc",
"0x89236f180a6ca981ccd89ef342494da4e8dc04f7f7e42889febfe3c9cbb3311c03f4dab9846b828a3f3ff94669516bac",
"0x8bd93d986021291acc100bb8562ec7775c5c71eca443cf682c9aab507400467a8a9f36fc739879da14cf9f8f50f86486",
"0xb9ddd4bbc087e5fd6a5cce0d21d142d497f49cfd078c0ca9c27ab0f3f5dde41001fd571ccb13eca44069dbb35643a747",
"0x87644ba29892ec195c66cc47b33724d31f9d15633e4748f4c083aa08158cdf21e33778f94e5a247b46a485ca5a79ff66",
"0x96d8bc88cc4e4b5f8cb71be343f05dd29b89ab13fdd33691aa169eea0d156af67df7deabee94314ea10318650f380765",
"0xa45fb5dee7aba8112920024ba029d2b23615af2648e1141c76c382aae6d335eada20ca0c503b41743f24a87add4c2f64",
"0xaaa7a332e371a849b6711b86cbb6d36976b61fa761a5c610ff4cc0dbcfe1dc7381eade5a307cb7ed1e6da9f6f7f42327",
"0xa486ac6f702a73c5fd7e0752d6e6962b5918962ff906ca64b450f658c95fbc48d59e727044e9405c9881b8a32ba1a93c",
"0x91dd9404e0dda11f2f5784c394803f213c6509e95e22d803548a9439ae27ae76026eba5f9f78c4ef3f73eafcb11fbbe3",
"0x949d97a470d36c554d0da958c85f0fd1621cf16d877ffc8a2957d23eff77905cec853b039f37dd8e2f1f0b40642423eb",
"0xafb9ba62f707059ec63c3a39b232faaeb5244354d766bc224976d213f825f6ffee9ced5f6e611ce3d3314976c658b51e",
"0xb3e23d1869da02c7ff6d55b44ff40edb199b8524af88e0ea3bff03b0012d55be9491c18bb97cae04f74f9356197e7d4a",
"0xaab0f191119f0bbccd6316a1761817b312245fd712b5ceb115acf8f86b7f82c70fd24cd0ac0a8617ebea09f96ba528d7",
"0xb78d30f275f3f3016683ccbe8fccd49d1a6e6fa10d06da86a4e1d72cfbd9f13866b837327ed2dcc79a81a5968c59eb5b",
"0x8a430c56f61a5fa20199cfb52ac535e6a147ab9687043f41458c6e87e84d4a57a40072d0e5a6bfc941a666adcfdbcc97",
"0x8a0a0d206e18121f7f9fe9719c13b2efc1e385f53a9188b3ad018af66abd9514d8499969d8bb58b524859904e6a32cd6",
"0x8a21a7a34d4c083ac84cbb1e5b1706e042669edc6e84bb23aeef9db6fde4a8fe22f9c7fdeb97cf4b3a19519743e73d7a",
"0xa5857a5f1ec2debaef32525829914b7a1668a3b52ff2a26ea48b0a4b0426a0355c0eca3c0c1ed92449bfdf75a39932e3",
"0xada21f5e0e6f21f4256ab23752bd48f4d48c02079c24a5ff2e7d77ffff3148de1d22694c2d94f65043d1d73e189ba3e8",
"0xab4c8587a33bd087a31613caba1c378b6e6111b47af9db3484098a04c941cabb1e500ff20b9e28f19868cc2ed3a13412",
"0xac0f11d9713e5dc46a4a43d66a048ecbca27bdb8f5f1589ab7e6ae80dc7866475379c2d7db0fa224d3ecbd36fbb12b2b",
"0xb4842ffa87ad2a0560a2939c1dc5f78aa8cb306a4e8d47233be3a590763acb099c22e08eaa5382f711b989d680a583ed",
"0xa3b3cc88fd5a5df2ef1afa6b70a7f39c97460e0b78b1594081e89d0fcbb6d69002fe2a30682bb121673e84132a73ef9c",
"0xb5319fe0d851ed2738b13fc4a6687520424c902942e72c175bab19554e70fdd397d4328dd2f67d9988742aa1d295d86e",
"0x8a106834fb0c69b90b03e3e3cd0b3b5bc083775ab0d30795dc42d7e37fdd372700776f5f60974d6aecc9d7a3160f8788",
"0xac3e836d6c610f54eed01704a05e06dd04def07d9fe12c73c4ffce98a5f99e6950632bd4adcbaa198a16e4d3a71a6c71",
"0xa5d9a61c29a8ebc4b5aeabd97c0e67d8398d66e5614f690bec37f775e1b3c94d51ae870add02263a644272c29b0fa3e7",
"0xa08e9832ecf90a010011578594374740814927d96e7bd9a3dbfb238ee470ed24cd7d93b8431c620b7a15e53427306fb9",
"0xa37765e8a2c0886fcb1c32ca9f338badcbbeaba45a0db564a752fe6cd61ec64592c637d4defdaa2fdfa6fa9515dfc3f7",
"0x8a85b0d436dfa151fb5bd2b3df5f1c154419059886fa9d1295878018ff4d74776594d7b20e156d444dcade3bdcf547de",
"0x8f3833b1571998fa57be9195519518c561a294f6e6f3e4c03053421352d72e0a9b2bc19480418dbce10c0ac5fec11357",
"0xa1cd3e2391e05997e16e9776f1446d24c8bafb296c13967eee3a4ab2c6dd24ead01991d33e82ade45b75dc97f00a0f12",
"0xb7f45ea9f523fb0d29a1353ccae680ea66304d8177190a41c230906022dd67f95711bf554f0ec30d1ee0533e719ea731",
"0xaa4eb2d468695bd198f7ce1054787f39406a2b6e3929a2173437291c274580e07e7e1c544ba481bad1e13e72924e590e",
"0x93e872b659ade13c32ea74866057438d66297c593014c25288a6141be575fe710c636d77d8e761d83a13168bdde4e933",
"0x97f754596f032abbc8e2ab8aa71ffc17275d132b2b4f009622cfd988cfdcc3ceb4c7979354481ff4c18af0a3f2c42b1e",
"0x90bd5f97e38f3f740cd1cd2863428681c9b612e2466fe0b2bd649b6c16cb92416bc78cd4d4459d7085a3babfef419d31",
"0xa94f67e0937f6d765766d48e2713008bb47a67348f80ac5f3bbf982eaf3b0356c05c324e165a1a36074acd6f2cd021ed",
"0xa29490c145a7c33ba638c605551628109a2f328ee05d30d28e37085c60842e5996f0e5e63c9d087ffa191244b6496e20",
"0x870745a995e60cfa4545f2fe6ae55045e573eca266875f5c92ba5b11f1b26ae6a244a6f75474164606a540ae950cfb5d",
"0xa3022e81838ec7c8fef8ca0849ef14490ed955abee69ab5ba48c375fe2d6e12d3d931c6ce5534841fe655bbcccf76d65",
"0x85dfea2494655ac9b82d2708da3ec3e2d2dbb0e817cb6f00e59e25ff53de21d27a0433a3268a664a99a2dd1cdd259dcb",
"0x95669f663ffe70e67af25b46eeb9afb86601e5deef3770c4dedfd6d0974175a88451a9ed847771b0842e53cf3ddd5357",
"0x8b55e41a5057d38b88712dc4a1649dabc7d38ecb996e2fa623d9d19a614a933251dbf4e895659fbb94592e6513565fb6",
"0xa13c4e9cc82631e7217d8557921dbe000192aa05e84c8038606fc51ff6f6782466ff98b320899f5632f582d48ea12b0e",
"0xa4899e162292b9cc13a4c8934346ef674d0685f8663785b9d50c9d40239ccf03e15518886c7c649979c4841ab25a0e10",
"0x94dbe72e948f1cdec8b960a2446ff6ce503d3a3f675de779426c16bf57aeca042ce2e0b094e60f1dfedc56cf9c4e744d",
"0xb488f3f8785bb01d42ec360660beb46511703930e14d9c3926ff3f9061d2a9e16bcc30cfd99f2d276898ba93f409693f",
"0x8080faf7973fab3ff4424387e80dac2ebeb6e3618c0054f29e5f2cd8ac4caad12cf14ff2f4f76fb44d2afacf78cc0fef",
"0x8d759531132862a31cac44662ca6b8fadfd848e793aed946719c2b58ac1a7a53aa8bc47e749cd70e1f2bfb4316334260",
"0x8918947325c09614b9ddb3521723a21ef977643158a859a7a5af014831a342252e7bcc359c608c212f1f186c1f564ddf",
"0x803a5e75f70847146e14e8d5b9f1c0a2cd355cfb8ddaa151ad91096be609a68773da47e3bba260037624b2cf89547709",
"0x831fcf30734803fc34220d29cc5e939a9d0bb4e83f4eac0e57c26ef7801e2834ea0f3f18efc0a5fb189f720a6473f8b2",
"0xa69b91fe7f287be29869b3462b3a312df9e10dd648e30c05a6a71f4e34b3e4756cc0fae4efa03644f2e897d36a1121d5",
"0xa201fdded1e4a5e49a7cdbe9265e3874ec46443200ac7b8df9900ec20ab8cb689e8f3f2ea6bb13e073d61afb3f26abda",
"0xafd013a39a1b2159f6249d84411923f15c458b009c639089ce17dba43e4f546ca1fdaeba62692b87caa1125f9ad61e3c",
"0x8d234bb8dd507f21e65ec21ec7e7d876bc5137614cdc745c77afd1fbac8d64a51627c3ae4de156cc6cd7fa3a4e7ffdb1",
"0x90e6c41e75238c3ba010af024f58cbb8893616f04fe5296f40a31fdbcbfe9a867969be2d89e71675a6655f774919bc1a",
"0xaf41621c6301884b11a87442d832d0a4d7c80100d93a39ca16100cd0024af86554e267a2d2a7ec363abdf732c895ea0d",
"0x8426c8051e86a4af7cc5641f48eaa4fcefc40aa8a2c42102db9447b87d42d1d380a671038ba021df8989bea77eb8f258",
"0xa8a343e34e38fdaf99c026a039768c776c6d390cf9a5830a9f9b32aaaf9770a0d3dfced4d704375f8396cfd5a02558ff",
"0xa89a5e97ebfbca0b61bd7d81a42af2c90f50e76a29939ec1a91a3546c98079262a073e741321f6c3c46358db96225cd2",
"0xb2057a06e5af142dc850be51703eb33f99cee07b4348ab39789d56c473811155bc6b64062d4f555ea8f54bf73dd69f92",
"0xb9b1e37dd03b29ef65395174814fbd79796b44ced47b0fe8b96b679893d682c45f241db1fa3aefddbc952ebfa1fe4ba5",
"0x925de4b7d055d3672fcb06437fd963a2b73c02aa7e595924e96737c456f00fa29e200b4f4589e9da1946f17c8d0785f0",
"0xb1cf4441d5aeb008b7dc186a00373086d1aefa275b6b5b778529e17e32198ec2ed9793bac75e25c6b12235e36efa2cac",
"0xb97bf89e459a049e74b2090577d49cc4818449a57d7df2d4706dbdd7532a6bc910074f6440023aeb09c6bee7e67900d9",
"0xb162d62cfe6a00e0d978ea2f6d8ab587cd12a14be2805646ae5f1b8588a340a4f8b6171bc092439157afa501f39d15bc",
"0xac100aa45c48a11686d7e2573e66723ff49b06af4d96d900ce6c08dc1ffd0e01baeada1fc5e7374caf126b50e33e57cd",
"0xa775f58424f7d8ed1a5adee9219e049180cca30b16b2502ea44fc9adfc20e2d00a4309e9a680acb2740063df3fe19662",
"0x8eb562c31dbfd222c5c2741a9e3db283d74bdecfe84b25ce3ee9e134a800445be360ca87489738e413315ac72b1c5e17",
"0x805ea03698e30932bfb6ea2687be573457df1393a7599c9d8631686a77e364337c4cdb54db14294e90366947941883ef",
"0x9486feee2d5bcef469e1b51f1a8a73bd7711923b4e126855f4dc8858c4fbc9b77ee8a56eb814d97534832e25375ce099",
"0x8d2f02f84ed2218f6f871e074d50ebe15d5ed2408d26b2ae6fdbb6984c02b0f2cfdcb283b287aa78723f4e75e7b5d176",
"0x964a3dcffcfc66c46b67c80e8511f695c6012b8f233cb18d7273d61d8c0dd27e35ff5dea42f1e1e9650e2008a6f3cbef",
"0x93c510e082c19788830888a30ebdbe92716488ee0a205c2b7d839a380cf716f5e241a19a3a859e2f80dec8a42c7d68b2",
"0x9376c105d2eb1f55e39332af0d70e587be9ef398e239b49b01560fefb0c950389761cadf39a32e324d71859c317e1788",
"0xb3fd7ba1d7401cdc8422e2f8fecd745dea850882c909e7b812de4067fcc5f745bd252c5132a9f83c1554c1dfa4c66ec8",
"0x958ef935d5bc06a876498d77a1cc2950a7f6990bf8f9d4ef8866d387f800d74b5833d608124a413287d063a23abb7093",
"0x8b338bdb7a1114f13bf88d21ae925a4c8487c96982e9e0f08062a11603aaf22273fdde9dc8807286b9bf91e2449b8ac8",
"0xaf273415edef8bd34a9b593531bcb6b631ef3f8f2db78e9ed73596d3d6943415df5bd1a587e99c378510540a12109e64",
"0x8ac7b7421acd1962ec8a91c8feee71dc245578265a95dd24549a86e8a3525b608c4dc08c5ed7dd79c78cddc41232c74f",
"0xa85c85174ca8b7bcbbac0e2483514326bebcd2f489fdeefce7bab04fb78c92609c364a4c2b3226275dabd660647eb3e0",
"0x8e46723134d662536ded0356d98de24629d7d182538a13143af285ed001f77c6d8df18b1a64e6f3ca0106413ec9e7afb",
"0x8b23bda51177933888aae4911eadf355cc94d8c4a84eb7f448f7ac3189557f0fa5c4be4af851abd3eecaa7c34fa30b6b",
"0xace70d08c5b8cd039a114175a30d8403f2255ce8f2f558b3d421333236886c1cc5afc8dd691d265b398689644ab10395",
"0xaa5b937a8505a3e504b37d5e4f7b36dbab2dfc70e18afb97dee96912fcd89997063df8ec843bf121c9bf4617eff7eae3",
"0x9977305cc2e85a095d06ee266d9adbda6231a9daf90840e3fbd835a36a6864b34909a06623f972abbcf863094816328e",
"0x92c092833083640d786a37d02c61b624243713734c3da43f75276fee4b40d2c8eb965ef2e9d9670e211a5468039886ee",
"0xb1c7da2b1303369115a611e872c7a399aa4ca5cabd404028dd52072e01ccbec5b2dcdf9a2e5849b138bc72022c810ac8",
"0xb8f291719beaebfe457bca35b64ab848e9cf16d0a3f628ceef371b370e51aa22d76af65ac5551a23b18f4a39faa4b2e1",
"0x82b589989909ed2c6d1d32d389d8becd01200425539d9051f29f54aea94221e7f1b272015f79dbac0515d1e4328e979b",
"0x80d0cef079e1e8f6a780fca47879fb7034061c6c06dcc2693a495b706a9802c53cb87d576c262fa08f435aa32244db43",
"0x9577cc69857028913d17fceb515add33b0dc600afa3d9b63ec3c6968d82652798db5adc2ae38f5fb2c0b7ac5a5f769b2",
"0x8a511d3ba6f7b4f7a711c1e60d2636c04b522da38875345923b0a7bb678c3e82590d0267b5263d4c12bd792f4f5e132c",
"0xb08b7c5fcb1a7547bd4ea9f41f0866fdc85a9aa61033d08082c9e30665663d48ccd027e1cd7d10510239fe69f990307f",
"0xa2e899191cd1c3b01a7466610c2b587fe618996b220599c63484c4ee529af1151782a94c14c63496578f1df3104811f8",
"0xa1ff0a31b2230b4da5a0dec6e319d00083dc41d79b7c47c82c82c8605b0d9691a35d88b10fbfbab8c59e46e9c4080b4e",
"0x8d430b795e6919b6badce0b418951301f93588c1b054ed569bdd48f5d6e8dbf75d02b4377ff1f8b95560cb4ba181c2c3",
"0x8624dd4a9d5098044c09466d33d5b66b435073bcfd58002ff7a0901031c3594ba650762d6ffc88a3a8b94477205fbeaf",
"0x993e49e0b4918e68f6957bcb5f53ef20af64b3aadee53aceb5a14fc298196ab93549718b54e76cdd75451e8b301497cb",
"0xb1a2f00560c27d32a4ab3f396ad40f1e62aaa1db4deb1c39006dbfba03489debbae7458c76ad72b039fb34c31117ba34",
"0xa9882d23d069a540ba42dece3a39e1f233e3658381519579120e42b500c5dd3c8395512f656ea7e78aba3d73083092f0",
"0xa21f9fd8759ae47fc212bf3580f359942bf1b3b4145eb638aed042a5142501aa5c224dc497032476e78007a491ca2459",
"0x90d6a0201f12f1a47ec332d8c516faf10453cc422a34252de1560f7624a0e1d9c20e3a53b42a5677e5b48fcdd31d7935",
"0xa16ff223344ee621c16899ac061beac3a582843f6194e5a02bbc9d874ab2e7e58012c0e144c02f7ab23091ddc10827d3",
"0xaa30c72c04063b1d662921793967e7a5bb3b97ad6f677d7c7f4aa4dd72315f2c3f8d85d5b6ed33905f49a842734fc4c1",
"0xa046a6611bfa2321073ba0a4dbcf87cf0b3f94c5cfa9e5e6be5dd90c0cd29dc5d348c4f6f9167838116b3111df1b5147",
"0x8b886a87b701b9a57b6be2a9a5903c149b8710cc7523c5127f71948781135fd0d56dd1d22f2ca527a9f69a41330adbc7",
"0xb3348862f36087d86c408888fc6fad04e77f74110b78fc34e15cfe30856c1647534525b00a8592f90ae914f44b83e641",
"0xa0d27975bef72262f888ad6428267cb8ece0a543316a514955b44bdf0886d2ff0a9e580f5211ee70cf385e1563c06027",
"0x96237f46287c98839dfb2b49fa6dde42c5ad8a7a1a6a99d0015ac93d3e053ab926b993c5f98c9ac7f4e94caaa6899ef6",
"0xb3fcf279120a43fd4e48adccd42ae8290acc86991372d23d02040e84d7949ac397bde6d4241af6db509968b4fe7990ba",
"0x8457306541e0d888c6c5b6d6d141a102fb3d8b30e279fbf49c9c8f9771b28eededeba5faef0f5867721f18d4e1918da7",
"0x9308534c4c250bb7f82a3c9e44ee5d63ced5e2871acfd06b4b24517621ecce952751b4be0944ed822c3a9fe6bb4a285d",
"0xa3d3475c09357bd8e07d952c4f6ab4256eb029d427f882e80fe2109910b88af8f1d18da9dd5bdd895a8925dadfd40a14",
"0x91381256ae84f35322b2a65d924afca1cea930484f55f5f4e65a9d4aeb81626add7cd0f9e59ff74f69f523466d386e64",
"0x8bc8747ab34f8658d0398a2bbeca402d6c1ae9b5dc77a7a90a139ece106f34ef81ca069ac82920000ed887b6ec7c111d",
"0x8ab97cb412c0b5b436f5382cb9068c2af675bcbb4915537cd2161271ee658f779ad46af5fa2db3a09c6234290cc867e6",
"0xb31ae2f6e63e8d5f9df27c07ea330e244bf332c584f7b0c02aa62793796e28ef03a9fa96291978fd0ad27130628aae2c",
"0x92325d0cc40757e5de4e2513d714ecf4bc39e5ee3144c257d290e06c7aba2199d7c7fc0013438cca5d1ed3e057cd6662",
"0x85b6c7f3f071199de43235311a369c92c332ee522e9a43da5759615718695f03b7a7d1113311e85a5985d4c1e65a28c5",
"0x94034a9a0e112bcf064ace23bbec3e31e3691dd31f7c20813523f12f1a727d1f87cee4d550f074bdcf0b0c9ca9dc5240",
"0xb5c07216d45b69e1871f3e2e92326ef3c3fcefa313f4214a682853e7742173b722004fe08a40ef61ff0079ae2cf677f7",
"0x8168732d5f9a050a3f8aa46adc05ae570c5881544d7a48394a3e4839a511e2ec3ccf4b05112bb057713f5eff36ed35a6",
"0x95b234dad219e722070e7da16b761c8a28eeff074ad8a154ad25b8f2c9a8dee34778479ac61bd51bf1d4c2aa91df8b82",
"0x826a4fcb0eee37fe69de350e523a898bbc2208ab6973c78fbf5f28714335343ed682cacca1f7233237cbbf4d0e9d3c60",
"0xaa3cb3e4238c6cc359224f222a7b2498fb74bb8229f449c79d00abff7d699caefb20cb0e7ab284bc7bc8e161df206639",
"0xa400558c0e1c36c22b251f05716a57af8a7c8089871f1dcd84b831532b3edb56573f3dd6a988d2c55bc0b013e0164ad7",
"0xa3870970003cd3537a1b0d0db691ce32d84f556287fc7c5e688961c676efe81d91024027b6d2456ff9c315c10f94447b",
"0xa32aaead7538e44b5ec52501ea42abd8e1f6c158db5ce0ef6cb0242963c2d7875976cf38b77d13d326cb36914152fc29",
"0xb9b665eb858c4353ad64332624d5bd317a073f88e41cef4cd1c1517d5cdf7fdb40395ffbcbcdbe31dba86ecc7f37cd69",
"0xb7707bb96a8b0bc7a6c7e6cbc2224b8d79fc80f96db45cd48e6a5b12700c2352e3c5bd34e559bf2fd4b47f1a12f85edd",
"0xad7d4977af9e831ef85991fab8858139c10f28ad40e888e23b7adb6c9e7facd52b38d8978cfbec1c57b9346032b7356b",
"0xa1d450c53027339add61685b096c41b45b9b9498b6627de8b072167c5fdee1127e9accc3b30af51a9890be0db54de234",
"0x86b0221caf110587b1aa27a0a2340cd66356a43896581d00bb7cb310e3fa387c56d03ee296ca73467047ebe0bca38e2b",
"0xa759f0b3a520717b3aa3b193276d1dbe01411e7433a03a8d5e48c4b1e6aa4c98e3d7a90084bd696137bd6de5cf53d1dc",
"0x882d19da7ecb524f2762becaa73b070e659d7bec3a6e05405543ab6a6e42e134dae2f5bb614c7741b70ee87f7bc63cfe",
"0xb84589fe4447c3aa9d857f2218945b6156527287d31b36043236bc779b96f36620cc76fc7f66a42c4e1b5942ce6f0dd9",
"0x90f98c0462b1c2653ce9cbef68a18631a2466270a33ccb8c7bde2280d3a476943caa359320b4dfb564d47baae576cc89",
"0x946d75e88c9fd69cf473d311e9e9d5b4ac615e1ae1394066f55cd41e40f623e4438bb4e185942f2c98e0a35117289003",
"0xaf4ccb6e0f27a9d023477285fe15106fe1f9bd0fc35643c07a653cdcaf30f3bb6a91a2599e08d7d73a3aa74ca81ad174",
"0xa8b3622f97290ee4cc0156447987306a22976c2d5a6c5a35ec9bfe9adb591199edee8f7267966a656d944978101379aa",
"0xa850201489281fad169f4911d5d1bbea5d1e53eb65feeb33c86de716838d221834b0b39adb373782227d40daad772913",
"0xb77aede1e6a4aab734a53a7bbd1cf4dd328deebac27361155a1719e682bec97381c79d9adefbecaa3a244cdd78afbf7c",
"0x92f57406af66fd1232cd11938f20b87311e6765eb9e84e632f6ebcae99d219538c8f02c72fdb67ff035205bd9545c54a",
"0xabe544f9baae40dbc866e07241d3b69bae9ea3bc6a5250bde7c29c3cfc1c3a66d8502aac438fe11bb9593bc523a62055",
"0xa324942b374e96ed23fb946171b4fa108cd1de6e2ddc81269134279a8422926b34df97e1a86f59fea9f4f4fd5b3fa56c",
"0xab0244f85591da41ae6b7ba8eab39ae7486123748997ce811bd64d9a3edf5f8f7333e081972a25aa5d54c85de58fa0f8",
"0x92d0fe78f0b39f3dcafd8d127008262e4ab2a73fd4056eaed26074d47dee5a14973563b39210504481710393da9369ea",
"0x8903c66bbbc31d472fc121029f9ab2eb5fe5cecb76b79c4cf41770b13ea9ba8ffd3457eb02b2a9357a4b95e4b34daf0e",
"0xb13f55bcbe2b7bdf277596b5946998877a61ea9d4856ef89ba53a7a2e30f0f4fc09bf8890b6154fa99a203859449e347",
"0x93a3d7808dd94434172f4848be6b19fac64bfa44cb876e6541e54dbc5a7c85ab02eab556c7b5893e6d4e770bd495d94a",
"0xaf10d2b17141deb640cd4de9c4dda8c99e15d0e37b4ae52fd1458c28d07e5b1f9ff1304d4a9ee6afd2e728ec9dd0ed3c",
"0x85839b47f0666be16cebea74d7d6eb6ec5da98f4c24384821e8aca25fec913099d1531bf531806108abf3edf6cacb226",
"0x8d785392212fc074ec334084f3b70e16655897c7e2c8c142684e51187bdfc8ce7764eb3045b98a38f6fd8837b6b06a6e",
"0x97629511182f8f17fcb413e3ea1de9f43d7018e7cb89299b80b39d613024dc2723b0ed1c867301cdeb093f4e22397972",
"0x9295f4a46938606af852fd8e2a1e74d34bcd34998f74ab1397d30c36ae09209e83c1883f01866c66aa583ec69e24bc60",
"0x83e2ccc4210d18fb8b8ccf934b16f9f215a1139a8af3c0a115f3cbf973a43e1043c2e7a6aaedbb50dcd60179c0d71571",
"0x959e82159d314239308a48e9094053a7a12e667b936b9888483760b86c8deb050e3c5ef703c140f5457a51cf443be8d2",
"0x8b7baf0f488f83bd46ac5d02e8c3dd615cb78d9576d4e62db01d247dcd2cb15aaf1a8a0253a087aaab49e3c04731197f",
"0xa81997d6fc06d45d544c9069237b3aa7a5538a8afe9171c49e711a54bc00ca7ea6d9b0332f2327e11c1235ddb8944e93",
"0xad8bee10f9bb91a29069b55ed79c0f7e0c31f574ae133f99237e4031e12e19611e4e98a4a0d180ac9c8b8e8e88db9687",
"0xad9e6b92135e4e2b817f45dc2943ad635da76ec384f2818924d53616de36b60ed62278e9325d71f608028c5df05697e5",
"0x86679fb63540f8b3190dfd6baafb6a1810ac2e4486cd0c80624ed5faf811a0e4df51ba4e352d85126276b6cb27ae4e79",
"0xb269ac860ae99194ea92aa7b68a1ea7564684be06b23364f28e031521297a05a60ed44fc79b93634fce2429c28bca972",
"0x9910e52b68a1a97a874080a10dd5687a2e536c7c9106e65c16f45ef6fdaa3dea20d5e0089f000c6005fc44af878c7283",
"0xb91859c182d3b713b14743ddfd6da5dee1f0f7aa964a2b79f4f2b78218f550128dfdef442a9f8df8a91c1bce494ec1a8",
"0xb96dd044adc70aa4943bdb911c10cf1d69aa78da9b0753e9da4ba274dbc9686d5bfdbe444a9e9983a42f77fb545c225d",
"0xa10dfd035212ffc6a568b2cb4b6f2d9a501db926f686b9c6932ebd7ac64a608f23bc1102fd0ac3adcf36279e9b41f906",
"0xa0057a101105641b0381dfa5955dcff7382e72659885056b88ce399d50c1706c340909c7769e9c02304a133ca8ee5898",
"0x96102cb239f7358b06029ca15775383ad1d438e4956fbe3b99848d069494ae472d1d9bed852b041a224ccac772014d2b",
"0xa741c8ca68a72e14e379b715612f32362dc1952017a710dada744f8866fe7b980fb653560f4e83c4c616eec7f9094db4",
"0xa3409f9b99f39f7ddd567f6504b2c3bc8140bdf0d1e635a1fc8b0c7377183c9154ab64e8f5753c1ba647e006718618f7",
"0x8c32ad2e87b0146010f2225dd182e88c0c70f128314e3d7032dc5baaa1713a0c25a19cc9169532b820e0aadd15e991af",
"0x85433efd595dd9f49b2604d421fee903f9a49af910068bf92c79f938a3001404031c00dd92bbf2385cd2d884ff428d9c",
"0x86cebb07a6b312b5fa55c5ae4deb0a726f85a953ee16947ae38de316b84cb7e307599d9964d4bc41c7e3e3eba50551da",
"0xa13a4db1aa6e6c58e1ea4be8a8ad270a326e9dccb1e20922b5a131d46184b41ac5559bb56516565967fdbe234afddc7f",
"0x94a66d71c99736ef434e1edff5cfd15fc03cad9222dbfa6b9f7b71b3259bb01f0cef0759959710fe307f19e7eb8a50be",
"0xb84b94694295efee1616f5b1f078249aad56f2be720593edb422909db286b4afec85583aba79ad2161610869cbb7b23b",
"0x95e24ae991a496af69bf6ff2e98a218c8fbadcb81997e2122259e00e1156bd957bbb7b56e63b451499307cdc5aaee84a",
"0xa2cf774951105d3ec53c0ab329c4c382fbeac36f24e2f8a0575c988eff5d9cc42a1c4cdf58d5fe867eb11e2b1d4c6ede",
"0x99680bdaeae98104c93a0fae79cba5197529a2802c06506758e26e41ca9c2cfba3727fe56476b329153d36dafa092429",
"0xb18ab0d63b90ffd798b3e2f3c8b79be43c85b4d8e103526117ee3ba9d14a486524e99bfa19c1c13877add76ec21a077d",
"0xb32f88abadc42328bafb33eddf360cbcbcd27397c64120dcc90afd4c80c80ec6f97b3a1c0a6e25b98e26b1e0a8dea903",
"0x8b8197a841f9893f0275296fb1f57f5697de753d8be6ba3ea76641358a5b0ee5fd23d16d53f4f459759377979eb9741e",
"0x8d0dd5d40aba0a62bdbd192b129f13e8203bc03acce08b09ae705458bb8c15719f8de6903e37f6f2c3466df1e8396d7d",
"0xaff3f5e10cabcc9fa6624e604d26ecbce7fd26d0b34b057a21a964ff23ade401e2a6cd77bda988a2b6b2a4c36fa36ee2",
"0xa338d853d99d56d53059d31de905b42d5b5480b5994082f91d2d87aa96faafabaa0efcc27ea3a14c768618502d19dbb2",
"0xb456f5b0b675d13cf537a397e9f7ec7d76c51617dfe01fd2eabd4e50b531f52d4bd66b585bbc3f56c8021b1927e297cd",
"0xac59fb0e115d239d86dd1c21bf66beb489f4b82284beed54711f229f979738faee0e6ab330bf1c05b9d9b669015f396e",
"0xb9f2a77117913a5c5378a5d4d59c6ba4ae380f8dfbe9d483a43a88c54c013ec4862e9186b8cfd06ec2f83cb077ff9316",
"0x9539bceca5d4de820da497357f20a4674f477de4b26ffbee6bf08dbfff8c80372b7bd192e24964f913bb2dd2a3d3c79d",
"0x8fa6acfbc88c8758e6d6dcf14be47b05ee669d9f763f86fe41c3fce243a126c5cd9c7f7034292b8e9b6f090d0f39cde3",
"0x86066d43fb75848eccfc3b43bd5a7a4da0f874d25052cca08ef93dd029e415520fab71dd315a76555b7c35d5c9288206",
"0xaa151bb710a3854ba341e4996637af9f478f85c1c0d979d74660503b16168c37ef1ac011818163ceb92438ae15f8002e",
"0x80ff5ea2842f428ad9914259a59b60860bd4e6e573c3f47d093a1551887cb6739ad8f590be086cc25f3178defa094a2f",
"0xb2d2c18d9457ba1606533570937f8902e5ec761e8ce8f9784d6632e4253909fcd4bc6634c410d219ece1f736ede92baa",
"0xa3155bd01319dfd6bd152ef7a349c548027ae853809e6c8b115889748d45ed627ac81fb7898aa81225d902bdbeb224f2",
"0xb09d4b9868cc7c1ea66fdbe8f129713dcf20ffcccd3a2f95513025a389efc24478ea8a850dccd74d1002f904a0f45072",
"0xad01e82c9d49c980f4021b0662a03cd740a97824ae03982fba066e017956c370d954119f6c8a353debd342fbc3c3d46d",
"0xa4300dbb6154a5907b55f74517b79d9ae1af452e0614768447ebcb7c48cdb5e91758d363ebf9510f035c135315afb081",
"0x897c9855dc4cbe4c2f2eed4b21e5ba3a009f2cb813919eb638083c3bbf2fa7f6bafa59f61bdb083c05e061126e1efb84",
"0x96caa39bbd5d27208320413015be1441e3c990a0fe997692c27fa0977e979e2d9073e01cb3247fbc92fe1566dc217c78",
"0x8e7270b698cb176292569516f62be18ffc315e97067a410de613e8586a18f7e19a11b1c40e7a570e36534e7b4762f0d6",
"0x95b82aae7d22fe483328d398d75cc73889cb8ee18de8256d9fc2279964cff2c20ae636f577e76096eaced79be649c668",
"0x9368f97633651d5ee902d87b431a1d0162f349896b3d8ee5a897c647b898599d2a0026584b7311438a918660a2ea68ee",
"0x8cdc5af25eee85daa46c79d4dfc0526b0c88d167f155f848469fcf7e3727866a67b0451eae2443abb8a91f46a6004a9c",
"0x96046065bc1a9b7f2c13f2aa5e573c344090da66eff7d0ca91caa49419388b8231102bd2e10f8c89a168d91a392b1728",
"0xb4f88e34774dfb2755aa471720a984b70ad0494a8b835d9eef732b7082fafef76da60a810a878bdafc6fc244e1ef08b0",
"0x826f863bf891a98ca3a82dff3950a0a0335f4d635b2d46abca7eb14b3d47038d09cbda998d6e3851bd41001e33db1482",
"0xb56dba2acab7be7284712a20324da40907a9210d78ddec77127928ffcc2cdbe6a7a5647c1bef055fa89e0793add4db00",
"0x84d66107df60bda44425262f1793d4c92b8584b973d4838ad222637be5132fc501431b7201c39bd8287327c1f18935df",
"0x83f81afd28b343a82c4b88c6c1d0db7258d9dbe4227001073cdeccdc0f2dc1b84c3b40db96414ea50d3f23a48bde5246",
"0xa90c68f55c1c77343c5251246ed431c0c4b3ddac65c60a75424b59ee35bf2d3e110d71a0057f613d88a0f848290b2c6f",
"0xb2431c6719cbb23ce38e76b488ee4a736838bafa76b4447ace74c667efa412b13a27b8e08fcf5d4d279db8247bd5109b",
"0xa8e6d94483a12d27ad570f199a9ddef6d70ac96607a08fa329e913722dd1093c920efccf47f9239bbfd0a3156603d0d8",
"0xa0416492b181729ee18066905a7af555f839bdeb30d31019c5a8822f0ce60aa9eb203bc1a144fb7c1c4af171dc2077f5",
"0xa367b6caa6497183e91c96fb0f347c638f10af942c29f03e7e127ef2ca61ac8acbe18022c80088615e4099ff2c6ce600",
"0xb4ab6fa53ffe64a61b1a9f09ef54c241b3f3f202966ae760deefa3fc7932821a394c3fcbcdc2f8d0d44498f961b5b196",
"0xa72eb051b039505d1959cf8c4574ce4be9de70b8b1846e51f98c2e086bbed3d5213fa5b947bf92084610139e1ef4df56",
"0xa99862e9b6f37da912255cd4dfceec3d3879766865d1cb7f80c823b22ee542ebefda6cd6e65e1c9d0659179420041afe",
"0xb4268aece8f8039d719bd002da7f1ad2b1b148344c4269c882ba588d744858bf8d7ad2a339c2c0e90c78043652a65187",
"0xa19497340f145a0fa19a488c0ec54908b66edc213f120159ea11f9d60f32cc460835f8dd2dff622cdea92a17214c7e19",
"0xa18648500ef9da6fbbef4cb6ed8f5c2dbcd461542149c7f34f3d2ace96ba0af6f938d700cdde0484d7cf4415ae17e49a",
"0xaebe6ad5614295905967bc90bdcedca109ddbe92a06a4e7d184487f044db67ab4c300e9bc372c9ab717d563cb7902799",
"0xb0cd81b5b26bd618312ea68e481a82432a79515e041e4e575089c5267059367941bb773d81673fd264fd6939695639ca",
"0xb6d7f9796e87d926c4cce21a2461d7aed5186219a638baae776d4b2ca99d4aecdee5e00f74d201b6f7a26e8a82b03dd2",
"0xb270337e07e1b25204e18addeb13a85686906ffc51996405198efe4e827682c7de3a68002932f9be95af0f149c49e8b2",
"0x84e47e48cf0876f2fa73cfee25612f1884a6f97111722fc4b2387ef6a6eae31e82b2b9aeb99566e8029928f2ff3b8f27",
"0xa961ee0ca2ea364a922d526bd4fb81949a952db511fc13b2ce8396ebf53ca7ff7ba60654373d4fe6ead27f1a7d07c5a1",
"0xb2779d398c462262fb1b5ecb4ebc50d00c44337e6ed20210ada4b8b6321b99ef262455e50647a61452aa2678e64def15",
"0xa6beac4c37c76e0bb175795d366178ab96ae0a3eabc1765c8983854822dc4bb11e2326a41c0847bcbfdad2c7f8a657f3",
"0xb9d7cb3c48d41a81e1348b21a3bc05d380a4e44fca5030f3926e390fd4f818829fc4e274bb22e1c8d5306ca9310b40d0",
"0x949b56bada8fb97e1aaa6a63903aa7fae44f412f9babc52671b78fd2fa3d1547ade93e54ebbf454730779434408d639a",
"0xaf8e0ebf68cf4f49984f29b045f9cce238cf69ed2d36f8652b3ebd7751716978795738b7113d6e3f41c912f31b5fcfd0",
"0x8f9c926f14ee78d6d4fc0cf19710f4dcac8c72e40d333790917eb7e233b60628fd7e5567019885c0f2e4e454f7bb7073",
"0xaf9ab693263d93cdd9e477b6675776fbd59812e11674fd44a5bd7049b6c8776f4480660d2f44dee719a6a98150fe4aba",
"0xa118ec8ccccca09b338e913c0acf5bb917926bcf2f48f52ba97f9971485b6b18cbcac42361431fe4614b2b40a520299c",
"0xac58b8a0d7d19a3ce3087f2b15ca749e857a2585d1b3eef55c117dadd491a0a46421e4dfb74f89633a1d09678a6317ca",
"0x96911acde9d9bc39b083edafc4768301c19f50dfaecbc3e0dd918ebc3e2f8c91b8c2d33bb54680a2f6891c73168cea5a",
"0x90af3bfbb5a7953639ed669914b13a8faea3dcf4af2c00c112196a4de4beaf2fbc83d1ba72ee1b9c3749cc0e5d87681e",
"0xa305e5ca2dc6015c07572a5985d5eb914c7561ee1e444f5df9076c7e9a38b11a95f28db3b53feb4eafc9ddf7d481d451",
"0x998e2106a2974c1370b012c01fd2e4fa60822e9f590be486454cceb7ffafed60858a8faa667c9285553819aa46a90b18",
"0xb0faa71cb1173a1b1bdbfc404f1378325935025bc802a5e210bdc63006aa9d83b64439e0e2c82694ccd5488f919f7c7c",
"0xb8ae8d18f5d8a5b7bacf03329a65f5d5e8ce46d83111a58ff8dc045c2d94e54b26c3f79a2c096c822d3a0dd81fd56e64",
"0x91a53eae8476239a7fb2e10ff05be47df93e1fbe8716d2d8a6568323c3682e49caa579c9348a9c59825dd2537f39b416",
"0x92ebf45084b9648ac36ffe91b2c6f15ce42aa5473b7f41e5d308847305fa2a30a70729449736d68ebf9e219999949613",
"0x89786449560b57f1bb488b0e0086428659801b0c7ff3e87d47bf9daffe3ff609d3a2b77e4a3e884549ef70b8aa16e1e9",
"0xa6df5f0147272d946d8ddc07a8a3e50866405a5a8b9a6968374a8550a8d5a0ab355f36bf65ba139d3acde7099baa803b",
"0x84af618b175ddc21598b33e1c5469a6b45f312f4bc27d5881479391cd20ff3b714228893ab05b81f40d88cf899b073fb",
"0xa13973bd367f5b4b9661bcc26ec6dc106d610e96f0d5dd9d0f451d1dfad0acda5fd375f6a522fdf58baae20e133827cd",
"0x8bc9e32be12843787401de4b6514e038ac608dcbeb2c4732d6bac5d6a6beccd0c41400b9f0a1fcde08affef34618ec68",
"0x826dd5d136d74f6eb10bf6a47f9242773830f71397919b7e0b42d53e18220a3532e8c730a8f7c6472b2aa2b48bd71e9d",
"0x95b39bec44b860590c0bec77a9c80519cd938cb094620fca4bebe2dd7a578188359b915f4b29103eb76807a4d0c11c36",
"0x8d5ecbcd0bb59e8269e3c50003455031824194d913d591e41a449d19f1e0c5fe0326d53a0d06957dfb8906cd4a75ea86",
"0xa6b0032fdb9df2334b86212046fed56282aa3f038b16814cd0b2806c598af57e3fa67786178b94b90f02d7f9acbfa5cb",
"0x95fdfa19cf1403dd221a5f0b7c6a38f9f171d3a1f4074f7cc272cdc96963b5aedc9eb72806c9abec3ab743d13a281cac",
"0x8f33a8ede0da0832b40b65715c4c2e0056d33f432e8a0899ccfacf321923a30a99468e6b83b8810be67dc95558861b64",
"0x89d7b1a8169809bd347259506fb80f92abc42a2a804b2384e29ba6245965c2e53f22018b691a9024dc87368e8af844fb",
"0x9793d3600db605c285fa413c7f796f62e79f3f599ac2813ac48bb74ac000180a3125cd1aec74d0f439ca45da3bcdb699",
"0xb93f409beff63139669695e4e20919bd3665038712fd63fd09a07e47bc8f924614bbbad715c55d6b393a14cc97e705ff",
"0x82ddb743e31b992a04a5ee6f6235a771e18af79deb7f3325fd53feeab7699f3bdcc91769cc4f4a3064192d6ff73fe0b4",
"0xb87a4545c62f56ccef2dc3cc294bba393df4b63a67bd6fb9ce41568aad4134d66f10105a41a43062b7ebe805493c68b4",
"0x95d60dfaf2dfb9defc0fe7555da49a68b847dc289c076e2639e48e11f116d0aebaa816f34958dcfb508b14883ee014ac",
"0x82c3c6a7fb9c0800179aa7fdd6a749f47177c99effd22b278efae2ffc0bdf046e087222ff3b5b911d037b1ff4d547c10",
"0x97dfdab2cf2be6d3719ad64a3be77a918a760ebf5287724097708a45833d4f82d868aef9e8ef5a4535bd7f89e18e7ac1",
"0xb7da1e4551561b4d1d5dd010f6c1ff7903f8a07ae9fb18fdb36804cec70c94dd7250141d0ed40fba025e9502af731f07",
"0x9749ffce13b359ab07a0aec5b66c302d6322f83e1696acfea333c823bbb99a8d9e81dfdad6ea54fa1bd7f8707173e399",
"0xaea9e0a4a251107cd9ef36a1f530487acc2daaf98169e602b15a46308286a7b1dc86cc5f1925e65ae99104bf11b63f88",
"0xafbb3f4474b95689059ff263013919e1d573f1495d23deb31cb2755f6bf9611f037366b144d7553b2a366fd1a6bfa280",
"0x8e760ace7e6b443b9655b44550946866495c353738bab1881eb9661507f5ee85108641a8e1a3217764f02b36f4e27695",
"0x8036cc56f2418f3cf027cee6357ea2f9dfbba27e21b3faf86e164ec1908e6e8ce89e3bbcdfa34ef5f86703e41f0a6164",
"0x8449b61c699086cd8b8fab8e79dfe5c30b9d1f0a29ba4b5b629e0f32f52c298500b5aec9cd07785b6345c00c197db3b3",
"0x8c90d9a3412dcbf2a0aa937ec9737abd5466712685f1faa556216a0caf2badbadca99c489cacff54208bb82ded592691",
"0xb1d383fdc7df32115dba9d25bbc1ba24ae0f1b5aefac87a513bc0de6444f00c409038b55ef826bc0f694e62fc1109b4f",
"0x88c00a4476534efcf45b74c29287f13827312bca83a1d5b487c359073951eac62a35dccd2fbd9fee5a956da29fa51602",
"0x8cddc3236f2df5064240e1759104370e55dcac8d592490e6e17720a81297d6eef5f9deef17a9b7b2db818dd015a8e206",
"0xa2b0fe20fe9fcf3db612ad3b1427310b23977ba9e7f58228daa0d32923db0f7319bc03d7d66ad603f927b0653c662977",
"0xa8bd0a1bf9d724eecd6d342e06e60c01fd9d6588b5b146520a24b4acac133bb24696da3968e1fd80d07badfcffdbbe6a",
"0xa281cb84af30f3989ca6a9274866cc6ed514b733aed3fdd623d2dada24845d487f11c3d41a785aa6c78465029987d066",
"0x811a31d4ccc4276ab6c9a20ed963b04c78dfdab1cf4be9267db0ff0199502d417bbb379a392ecd4d68211e7e48233328",
"0xa4c43910dec1f8db35a2e4ae537f20f865a08bf313b614c66af4e6c682f67bfabb7c4401c01aef0cf9d7a50de034c2fb",
"0xb2e85a231753a8158106751d85b725d996c3f4e85393fa488c2fe1eb8dd1a61e605e26cdeb304cde9a67095e57d8b495",
"0x91a89b32f7ad2f4c15309befff4ed2755a4dced9c3da7dd8ca8c471888e4d3961779b6cc821c8dacedf60562ddb174bd",
"0xaeaa2cbd26d616a00f08a39427f10ce8247ecfa03a3e1660b991731e8af499466f93b91f6af3d2f848bdd768b36d9977",
"0x93d5b5ab7738e739ff410102597b157a3c075278562a46a136c42bc92be9e4dcb0d3c707a11cb7c485e90c315c8b6897",
"0x988ac3ce5b274cfd99238d073116a5ff9e43c66a21c7a98b3f6715a3a12a5fe8dfce1a3b746a2582a379d18317d4bcf8",
"0x93d23a114b4ba96c6050f59a2d0c4440020fa15e4355065e3a98d075509744412e08c4b017d5b131e7b47c7434c970f4",
"0x93f83ceeeb39768c36dd53e03211d70dca278e3b9f62a09d3e5b131aab0d3c90857c1e6fc6cefcba776a861c8fbe68cf",
"0xa38f5834bbd02d778cf0271e203d622292d60a4cb304574372c511fc40975f977b478b36ed2504b6a09aadca73195a32",
"0xae98edbc84043997e689b803ca8e1c135abf5381c874cb191d7a2cc490276bac5da4788506eb0dbdd5b7a5aa2df8c9f7",
"0x87f714197f65adc2d1ef88993c081b09f9c41f278175234d816c1f84b4e4b80161b3d67f4fd6e0eeea7c6e06847d5128",
"0xabb630876f09c6917e46c97d6e3154528ece8274b26fe9825d38e40a3290f1849d21639e28dde6b1231739d0f34efd23",
"0xb892f6b8582705faa67ae814fa4eaa5a30a097c178f53f8a0b9d58dd99b5dc914ce48dbc9172772a398464425e322bb9",
"0xb140718f955286f027074962bcf413958d66ba282f7fa45a4c15c2f6fa4b1a23e9271f47b3c8e894a66cce02279827fc",
"0x87b5c395ddda461e84d36cccd335afe71c9c5fe8ff7d9a011e56af5a4e2d5059b5a77c4c40f4582f83142e01663ecfbf",
"0xb86f0a61634c096c1547d3f6d5074c7d41493c3d5a4cf0b79b441e2ac8c10c6e2769c3737627594da32fcec25cc50d0f",
"0x8f157cbf5a2a2d1582cdf2c1a013da3aa04bf694209e2cce5c3efda6cc07999b73a57d5063358646c35711d951aa24b5",
"0xaaa675dd6ce103c6374ca148647ff4518c092ecddc5652b3e8d123a9d2d8b5ba1a878aeb275f224b50c0e6d718729501",
"0x818548e74a34e7f2b3f4e1af8f72fef5d6c96f5da596a02027d11f8d4ae93f8752d1a69bc6796b5370816f0586d5e49d",
"0x91b640fad117cd3a18e01605e12a5ec2ae81d7857529f4b1dcd98b1bc30b438f9583cf1ab7f2a3d3ec4ef342c63b10e5",
"0x8e5fe6dc7c5c220680741db6891ea41c239419a08af8daf4a4fd7d89499b1299679a32a413074181c0b83a6837e557db",
"0x98158add05884e701ea06b0365d2c5ac977b8502e570175f987ad7d5b09d7cdfd70e18003165e82ec8c53de32ceb251e",
"0x93aef40c696ecbe9058952c7abd5700aebc2144a89c720d05d99bfaedcff712d97bc0b5cda0b2aad2ed9b1b0aec0c4df",
"0x921b1c7d631771e28dc54846b8d3076c195cdfad1955b7d6c3c97befa62a02b779b65279679cc1dd89d50f0ba7afc8cb",
"0x93562f10036add83b59f62f2d7448c99dabfbd3f51c50399ea5f546c11ec8afc727b7a96732af41297978c0569140c3a",
"0xac8d6d26e3548c0622652c103361a0b2a7d6ebd7bf3cf1ba5b6d586becb1c61b166a0cbd289596b52e7997d245fb9050",
"0x8d1125dbb21e4899fc2438da5972a18e7d4ff49b1d48ff1c7e2946c0c4cd29ca7840ebdb30d3eecc80268806f58d20bc",
"0xad46ed09c5ac5807ba587edfb180984574e55b551d94d88a8db59c1b1f1656e98b74f39918957a0b9f3fc9e0f91672d7",
"0x852efa4ab9ad3114fe145b1f31031f6914240162203afddbf49cde3b1ecd29a68bc6c9a8df18aaea8b23ca4b77d8d38f",
"0x8570af9e007ab0b1d82f5958a7d8c91aa0cd006a3e43138002833cc5fbb0bd53e1db536d2ce7dcfd13a2a06e3b490946",
"0xa676181c8a6f628b907a8d60513a15d74bc51fa3d83343ab0627d8ac4a14dabb122c287d264ef2ecee03fb79bba76114",
"0x8f0c8c9cadcf34820f162ea8584b6b61dabbe16a61b0ae4faa03305c393c26796ca2106ab8a1fddf17af71b063e63524",
"0xb13a75a960eb73fc858807237666d7ad86c328cf74da947bc80cd874afc1cc3b417a90f75b0f3558f0fe30445a163333",
"0x9141877bf22764a12270f232b4c812555f7840fa1c1c67b4ca6cbe5d10ccfdebd7b0690a70dfc99d1366fc56aa5fb8cf",
"0x8c5a115c9287c550b747916787e4d671c628b0d0b688d3be91622ac66302468385429d881420232fe9eff14b84af4793",
"0x8325ed8de505dd3848ece76a2425c34f35ad4a488b77107889843dda4646be95458caa090e3a4c3c0c9cced560d57b79",
"0xb7727e8a2a246b6ab264efc8b8a851fbfe91780013f88b1d686ba6e2dd3f88099ae5cd50b087eaee4a229a209e4e11c1",
"0x874c00eae6040f8f371b3c68fed8279ba292f776f7ab0d25364c8bee33f0a865d36ef8285a92c16b84a9ceae684bc7d4",
"0xb7531fc6846e16c5950362cece96f172fa39cf8dff71e37b0ae142f74b25828cf91bff25ff91687977dc13e875942c6d",
"0x877b381025d9d3e286d40f2091514bed9a664410e58f1be36461bef1943f4fb4c63f88b1ee3115c25550868316409c4a",
"0xaca83783c5621787be754db26c5711f82f0900835a840746d01da65bc4416e5db432124bd743fee77845414974c2049e",
"0xb8185e31d16c9527f6b7311be0254f8741f58180724c4c983d2db9b2c770fbc654185b4744f5ef7f03c1b94cc2abcdad",
"0x92e2993a2632dcafd8ba986c68a1f086424dffa5f6038325c7d4effb6c2f0673f60ec57d62cdf278d809d7539b395aa9",
"0x88566790bda485f19e689bbe24dda0a977f97f199ce2633e266b9e88179a7adec98a465670ad97552913f9b5a4d32e85",
"0xa211b30b5a60935e13cbabd302216058305f8a1c3d9b53260df052590f9999c951e291dc3520510357835bcf756bd7d9",
"0x8c46615a602aa3c5f162dd0d378853b5625bc081c8da5bc68450029b53f7183431a6cc41ab4a78fc450f8d4558db80f1",
"0xa2af75fc3e8ad935405d9fc72558793107aa37d83c2f4ddfb734c747ce0afb36b217b72a1779f12cf1e647bc3d1ab543",
"0xb4606a0a3014dbd58b24859f08e1368b9e5a066d89ff2124985d9820bce141c52f07fd2bb218e963e6c05d785abf850a",
"0xa285ac44a970485c2d705fd1487d5f4b5d2fb9f1537c891d0eb5936a7d4ca50f74c7a6c660cc19a23757cbae81a2c455",
"0xb720f8dfb918aee85a09feac2ecc48ccc41545d9bf7d5a95a0ab8ddafcd627b62529fd20ed013c516b34220015fd596f",
"0x9543688acfcd61632778782c77230aa53ce964a004cb3186be5914a9a62ccaa70f1c72734a83337c89107fdb2c184b9c",
"0x91a4e2ab7f3efcccdd433415298344a3a98516f529e02f8bf9483ab9a52d3a814c17c7a6b40c87302e84242ac4b7b4d9",
"0x8eab08992fe9606efac72326ba6e03c9ac7f3dd6c9997d17346e87a0eb6bee47f94e7e3e429b2fe1e159ef38a3a917e3",
"0x97f76b38b6a5fc51c7b476a28237129d0382196efc9357f1e5590fa5217e4496710ccc9beeda38f6cf0d5f01c330909c",
"0xa076bd5dfdd2ec2ca7bba3116996210147cf9b81f4cbfe3518ee13f9b53eb762f2c2165f70e532da161b0c6ec98d01bd",
"0xa9c7c16704060e56e71a08aa2f6c6ad8b0f22272a8d64f13a4cce07c37466f45e94e963c20ab799c8230fd5bafa13b88",
"0xb6c3420776327ced738aa33ac2a4066bcb6b79970172a602125f8d5516c2e247db5ce2ece8d1961f5daf9c2d75070d5d",
"0xa80b7cd2f212be04f646f11276fb2cffb7317f9c666749dafa11507fc76819b9594ac03de562d75219dac77e86b8c5e8",
"0x8d2b8f2d0b3c067a1c17bfa4cb6fd14abcc71dccef549ed919414424fe3416a5f7980a524882ab11362478e25bf79247",
"0x81eb2c94c95572089095f5857cbc3cab4cb0e0f39d154bee873ebb921099039ca09f374a66dcf3b170db9528741d6b2b",
"0xa6a1413c8267c31e14b4ba5f39398630310dc26a3e19841eb3af16983f0b6a7005968c602c41e36eabed868b14d7e072",
"0x8eb6298d4445f3ce9c86eb34f2907d2583934c810aa94f8b91b571da3cc3056de86701ec00e6d5ba9ce90d5d56c6138f",
"0xabf9926830ece12b985435b97cb9c5e52498ddbf282cb6ea2a280542058d3834ad77ed05d512e2ca8e9e13a91d9432dd",
"0x8b55691fdd7a96a67700e9c312fc3152b524032757d6786f591c93cb3a4a620c7576d6a2bd054607ca12952fa3622724",
"0x8547b122267b6e1ab1632e4ddb2af5e273c4323471efcdacf16a77f1c5a97817ce0d59a686842d6d45b5e350759b37d4",
"0xa0c3b915a9b4e9c20a55354a7d62e4cbeba0010aeb2326bf9365cbccf1b8b3d18c6eaac6c793daaf5cce15153af93483",
"0x8c770c76f380dc1bfe9e79cf7f60e9868af37300f30528380ee6b6f161e7ce09a8a1b5a5dd23903cc551a52c43fad6d5",
"0xb233d0349242b965e6c69c4d286fe12c378ffb3ddd8e08938098298a0b23c59e0686d2a190b28a13d86b5ff618476959",
"0xaade99bd0c2923fe2df52bb8dac4321387e51f3256eba99a7f0645b0aaf55af4885d158eb427d9e84d450002254bd262",
"0x95050a292a0bb69c866d92bf1095581ad2aa94ed585b5760f794d4de947e313f05171025c2a85b5a825aace731933c19",
"0x803eb6752b16d33d92c84e2abdbf1056fd2f89df51e00734fc33ca6f9b2a2bdc103e6bc368cca32a6616cdb600284c3d",
"0xae93b201d1dfc90ec61cfc379ff1eab01558308a22a77a32765eb8cc27467fb86fbf2339011f525de26a4e37a9d4d060",
"0x8a4c740d4a881d4dc0514a212f8cf88d96c711cf660956566e1026eeae96b25c42ad7474046ab08115c2353a125841be",
"0x8afa88a77d7e847ce9b13201b19764caf07c79666f150122b133bf29eaeea07a2c5d7d868bf9a983d426ef5977efd7fb",
"0x95f586105b14e1bab698e18f94a0bf2cf223f092271d28c9f6e72c3de88d6232527d85906190b77f953ab4289ca2ce39",
"0xa74adf3914fdc3717beb02a98ae0223d0c6e093220a43504ff73c9fb0225bbf145a718d78dd448015143fed8bea66d2a",
"0xb1a6f5883ef2afb4bcde5cc018fd29e56d4d7e65a9e829d7f5f07d3d210deceb48dd4972dbf740334ccc99b61e46eba6",
"0x851aa8cfe9538ccfa863700af3fe7395bdd3d051eff006e86053e912d1d60fc1bcd87aee23eeb8ecb3f547b17dfd53ec",
"0xac4aa49cdafb0847f1f7a9bc2a44af01ee8696fe189f618b751dce4cdf9f041e9a1e34d3c096317483f432df7799db33",
"0xa274d9ce5fe87a0436b1d6b606db605c1fb27594aab1af81c2e185b2653ee15fe1526c5e6177a44fcf9cedbb2d4793b8",
"0xb971066e3bed163cfa364fda1e11dc7c8176eb6721d98937d29d5103c3cac1dcba1d610e54bac9e74341fb3d4c5961ab",
"0xb6c74d8a586690b97d10bc4b99b2e9acc1dcb1d58634a327729d699fef68da9cfdca7b408fd74efc267eaed553257024",
"0xb827864273932536c3135ed80e9378e27e6d21d73157ec24ec31bdca4ab6457be1dd4830671ba6429a74aed3c02fec32",
"0x949894777c68920cdc9de840ff2748633964458e4b9dbbe18b21bba5a01d3f0357611d3c59b2fe5509a4245fdff89f84",
"0xb9bb52f7147392b8f0d1662825c64e1caabaacf11b7499c3fe6f9d6f86a3a51548a75ba8e9e5fc5aa656c8a34aaa6b9b",
"0xa32b694ffc3ff7005e6aac7a988950e4f2da29b567e38acc73207f95ba3f7b3594de1537898cd9b4d1f5907211858f26",
"0x849ba686af1f6054de495636a2301805c8c01da3470592d2ad3d18d678de22ae332c8dca6b8420e10f42361beb5baf90",
"0xb99832e7f0cc9e018bacad7c1196c1be70b6abd81e389d977839ce6662ffc981f10657ebf9ac30e23d7a27363eec4077",
"0x99c6e19e57d69e577be61c6266f81cde376b9f35abb0184a2686882e4476967439862b9a530cae0ead29b741d2eb172e",
"0x942de4b667daa85d48e352b051ee29c8f3752b90ee92863f854866f21b99c97babeabe972224b4d84db621ca782ebf4f",
"0x85d94096a35da022155562bf5d21f99228062ca203888975d59d2b3e91fa4dff6dda6930f304f51f9f7aa54679b82e6a",
"0x822544b8a8eeb609f2837123433d0170ea512a16b2b10db245102f6026599a042ffc521efb92eb141a1d2a4214bb6917",
"0xa62e2cdc1486f555f0f58a3b4abe8576388e8d6fa650656b0d4fbf75510768a9f45a889d9171a2deec3fd98f50eca558",
"0x833b9040774febef4caa97b47a825cab0d1150da5304be94bb7e5fb36f2ce5918bb46106a987f0d8d8b4e35c86f6e308",
"0x85ca18e6f8c9f5e0d4e8c2d203395be3a17b941b15cbf911e581f99a4bac640c399daa180820c1afb1f591e5416a476b",
"0x837649d4fa89066ec02b4ea6bc0839a3394b26c38e2a46e75dc2da15ba8be23e9a7f9f66015b549beefb325b5c77f1c8",
"0xa2d82f55ed9a3e31133708dd5aa27a37d12d5df7ab2f00455a3846de7562d27a5d06a7735dd506edf11cb10e93fec320",
"0x8e5e3ad45313da6bffeb52325561188611d23ce9e38c781757b591987cad3b14f5b8b6889cbd550888128619fe3f1b5e",
"0x8d037d57cfe814d91dd8f44431ebfebe16382402246a89303776fae8c746709fc069b0825227db0a15b5cd75195db910",
"0x940c9c5f145d64f1ff2dbe20c73361f8031aa5c28243d4740e0feea66c3a9922e63596f49d5ee6184a58ae92ed126a2c",
"0x9165f88c51f25491e3c48b5414758ba124f4d43741977665e1d910b29e2df84f0a615ce143504e7353ab0b47c46e1646",
"0xb1f39e3644cc1ed40328fc0098dd4064a258722cf65e80dbd9cd9ebe0f11cab31842fd9483be201627e3660500ff2c23",
"0xb5e47f405760322b46d37f63955bea7555f6065ac17a580d2eda371309d48759aa462bfd4067aac6730ea6391a9bdb0a",
"0xb949285d17dd61438eea643d1ff0f85463b552d98ccb4a65c9439d84e43ee1db49f3af027e5febb56e1f6f5d80c91412",
"0x8b6ae62bb33cd0966d7fd59d6b7f92c7c258c8c718eda58bc0b9ab61e60ca94c82af267d7a2857f694f0c517c7593a6f",
"0xa8709ee9482be378884a40098e5066c9e66beafb454505a7a300f38f102d61ed70d5ae00e9c248b9840cc45139d0a446",
"0xa5cbb5daaaf6fc5d5a9caf82c88666f33bbeb695997eac016995c687bdf91cbad2b9f32213bbe2c9bc558a4717892056",
"0x979c292e65c2fab5639a5d684dc1529888bd60f0aee97dec899725bfc9e22a98fd8374d022d366e10e2f0a3d4c3e6b66",
"0x82c0597dd33bc8e9b3b3e66a0dfe80b3058003d733518255471ce634dcdbd26b12dfc2b7ed7062181a84fd939261ecb3",
"0x95e1c90de7bede21c3270301f6fb9ad6ee18c86604a68999d2421be3b09b66299775cabe49c83658b1c34ab132a240ba",
"0xb6c3e5fe6262b8bd3472516d05fa170993dfbc24adb4aa5dffb8b387cad1a4024850400b27be8ad7a88c6ded0cfb40b5",
"0xb652d5448b08a9acb8820583d69037d6eac4538bf01100aa2ccc0a35d0e741511047edcacd9e1fbbbf144dc7a68e3a62",
"0xad26a763e5b9bfcf1e9da160227ae3184ceb0ca160e497ed955eb5474cb1fb15222ebd2b95bf5f79df34b708826b5135",
"0x80bd2a8cc1238f91209cb483297e3fcbb7aa6baf3124a75d98da96a075107c0cbe4cbed9fe8bd901fc5c9583e68f8318",
"0x948f28741b3e4cf9eb2f1e14620c011030b6d38ed2cd0ee9fe41ca5bd54529cac5c7b7f581212878ed48f30a298c05cf",
"0xa34fbf6ef71e8bafe5fb98e7ee99de5fc60f6dd8f2b15407d052158d0f34a7ebce63490ec71f4da534816840be83cfbd",
"0xaaf6740dc86b6c4501a19068e190d44b9d07af8e2a3801875db6e3d183c10a86c61e0d362d3c11bfc73a1f8c9a55183b",
"0xaee2373d7868128c28bb58637d85c148f1aed881d8157090ed998af6d961bf1dc37391294b0e361f5578d3fcb5bd60ca",
"0x842edd482f9bddf87300b2d1c916f141340691ccb6b36fdd396f50cf79fb856d793bd50c8eafef5c949faa33d57ad54b",
"0x8fb98a9e82933843b995ec0d929fb775c27efe51f01e9fe3d4abf11b03281cb6df26c3b844102902097397ba8a4ed95b",
"0x890d76989026e2ec0ffb92eeec65a552ec7671c17cdad6c22fed7aa7752a9531304239661ea07ce12b93404d1d556127",
"0x98bf7919c81f8b14e6ebda09844bb2609860669a28347a13c303bc62893f62762159fb8009ad9a8fd4d320ee4d2b3cfe",
"0xa69571f3cd933820ba3b81c20630a75a49aa6e9a019cf9815da31efab1d906fa0d02dc65b3726460de6f8dd001ca8526",
"0xa21e6285c2dd8ab627e9052440c859e59937d83d2ba9b85f65ab7f9249c4a2de746b3feaeee4350d478fcdf0421422b6",
"0x90273ef6426d6b128763e367507615e6a79220aba9c30d8a194c5da350c065bfd378c061b6e3e57ca5dad6e430e9c6a7",
"0x89df93db35de1404230f5af7561385929220d5cbeaca1559bc5f3097458477b20739847b707e52e5cf31edba92529a21",
"0xb3fe1edd1d8dce5efb068dce704e073a3473b2f78d04b59e43d3b96d58778d4fa00d4f08f60a267c14e61f8f629f7205",
"0x881a126490d54dfedbb4bd1e951f8cdfeafff7b164a1b4786ec577cdb7c9a75b29e2c65ca1749e0417fd9cd2cc3a3cb1",
"0x810646bb3b98b0fa7d4541ce5cebf5526e501807455aaaf5c0e7a393f832a93e61b701b5e14c713ce519979085aca5fe",
"0xa27e64f8d9cad96f156bee43b5002970f034149ed74833a3a85cf5ff7e0c73fbd7704c696db98cf79c80e37c1d091d59",
"0xa78dcd5e2b7934fef51673c465419e2c057399f792e36002ef5aee09d2efa0845b4bc8e001bd70ba957a567d233159e4",
"0xad4ecb76e8a30e427af08dfc7fdd37d3b69ce3f41dc374d28bbc1b4be3b82984a3c32b9792fbd20333a5305a7cdd23b4",
"0x85c01bdf47745c94c1e93e92575133c757a419c91546d681902d50b94dde97f7d3fc0323337f791283762026e0ba4b17",
"0xb9cef0b3c0055475dfa4f8bc9c30ba514be1caca38275a1215215c8731aa21959c7097582002f8fabb8f3bf54623489e",
"0xa96e8620be447aeb3eeba0277da17537206f23ffcde12ec63fef45d741bb28a973ee48bf5be317caf7a2d172e38ab550",
"0x8dae5a64d0a980ec81caea6ae9971f0723feb042d30e8aa6a109a1ed8ae5689b05792d48b1ed54d95388fdc618e25505",
"0x96130e73fb0cb8e56d11e2939c977999cdbf01bc0598b228d7485067b55d9b9ec896f0490afa2ae6699b802585b0b00d",
"0x85bb03bd0cca1030979a01ffa2dc8178bd1add6c739179f950e21bbd576b10f3dd41de5148f3d4eaf0f6019bacfc19de",
"0x8155b105b3f0d6198cf80c62aae83185182a717189c00c6180a6b620a5c094b993324201c4638d65a95e04210b41dcb2",
"0x853ccde301148f3e5aff12409a29f87deaf319bbd0245b0a20f10c117227ab33a5aa5ae81447aca9c0dc708ea7d809ec",
"0x909b5a6b09ffd5a64ac1cdce0143d09833a5c9bf5c5d190995dd37e5945c2c89d3aa5d1cd7cb5a46318752c8b6832152",
"0x94c5d61e616b16eabffdc6fe35e9bae55d8da01580ccb15a18802502d1872484b386b50893801ee8c15c8ac7ed7ed176",
"0xa49feb98f5e8872446e5f8e919d20b7bbb82f2fabe59458f416deb4fe2f415d579bd7ac45b286d522557c53e7df2b82b",
"0x9071145e343177cfb81dc5e68648309b8b61636d30eb0ab6624bae58e8992d98951c299665a4139d12d4ea2f505ce265",
"0xa68a488b0aaa3b060c0778a6e87e4f2dff7c806abf0a2125f96bbf3f0ad191d32fcf87d27cfa06eca4a9618eb48b2bee",
"0x991febb335fc4eb22cd341abca8320fefa132e394d115cc8d6ca5b0d5de5487d458c5165a0b504bd85ea8c842a8dbf29",
"0xa12bcbdb57fb851f0fa9ea0e1caf0929e8bef55ac71bb236c70124abae06581bff34a2b93d8a063e2bd42711ff535316",
"0xa6a0ee28bf7027849b5605965c14ee75db82dc5d9374a8fe568fef6123719ca1698bed51f74fe872e6075c6ae0d3c8cb",
"0x8559c5305305b10adee0b87536e18b3e70978b4f458531aacf3af132fffa68f7111c5155fdb1e5aedec27b746a99dff2",
"0xa6a5d24dd7822c51f5b2fef526c735733a2eb5ab9ee6848f4e0777367bb2fd966e830056a54943f9ad9ee712dde5866a",
"0x901b638eea2e8e7b1405ca19b80ea9be8d84fd117b1f8c22bd176a918fd5093d38eb720a423df577b8f219ed2fd994e9",
"0xb5810330244b12529cb2596b4a3ff31305f8aab243bfba01da770b5754ee2d73ff7c9aa657a9e0cd0900410207c23644",
"0x8451719fd8c98232f9b9cbb1ded46b1caff52b3d7509533016621fa49c461f129c2adbcd4941a500fd5209c9196acaf4",
"0xa9749b942997de005582043d60ceeeaa6ac945c688ee3258e46ecd927690cd15412f827a6bd43b42c26f3d887415ff35",
],
};
export const BEACON_SYNC_SUPER_MAJORITY = Math.ceil(
(BEACON_SYNC_COMMITTEE_SIZE * 2) / 3,
);
// These are the rough numbers from benchmark experiments
export const DEFAULT_BATCH_SIZE = 200;
export const DEFAULT_TREE_DEGREE = 200;
export const POLLING_DELAY = 13 * 1000; //13s (slightly higher than the slot time)
export const DEFAULT_BLOCK_PARAMETER = "latest";
export const MAX_BLOCK_HISTORY = BigInt(256);
export const MAX_BLOCK_FUTURE = BigInt(3);
export const ZERO_ADDR = "0x0000000000000000000000000000000000000000";

49
src/interfaces.ts Normal file
View File

@ -0,0 +1,49 @@
import { BeaconConfig } from "@lodestar/config";
import { GenesisData, LightClientUpdate } from "#types.js";
import Provider from "#client/verifyingProvider.js";
export interface IProver {
getSyncUpdate(
period: number,
currentPeriod: number,
cacheCount: number,
): Promise<LightClientUpdate>;
}
export interface IStore {
addUpdate(period: number, update: LightClientUpdate): Promise<void>;
}
export interface IVerifyingProvider {
update(blockNumber: number, blockHash: string): void;
}
export type IVerifyingProviderConstructor<
U extends IVerifyingProvider = IVerifyingProvider,
> = new (requestHandler: Function, blockNumber: number, blockHash: string) => U;
export interface ClientConfig {
genesis: GenesisData;
chainConfig: BeaconConfig;
// treeDegree in case of Superlight and batchSize in case of Light and Optimistic
n?: number;
}
export interface ExecutionInfo {
blockHash: string;
blockNumber: number;
}
export interface ConsensusCommitteeHashesRequest {
start: number;
count: number;
}
export interface ConsensusCommitteePeriodRequest {
period: number | "latest";
}
export interface ConsensusBlockRequest {
block: number;
}

209
src/node/client.ts Normal file
View File

@ -0,0 +1,209 @@
import BaseClient, { BaseClientOptions } from "#baseClient.js";
import { ExecutionInfo, IStore } from "#interfaces.js";
import axios, { AxiosInstance } from "axios";
import axiosRetry from "axios-retry";
import { Bytes32, capella, LightClientUpdate } from "#types.js";
import {
consensusClient,
deserializePubkeys,
optimisticUpdateFromJSON,
optimisticUpdateVerify,
} from "#util.js";
import { toHexString } from "@chainsafe/ssz";
import NodeCache from "node-cache";
import { DEFAULT_BATCH_SIZE } from "#constants.js";
import {
computeSyncPeriodAtSlot,
deserializeSyncCommittee,
} from "@lodestar/light-client/utils";
import { assertValidLightClientUpdate } from "@lodestar/light-client/validation";
import bls from "@chainsafe/bls/switchable";
axiosRetry(axios, { retries: 3 });
interface Config extends BaseClientOptions {
store: IStore;
beaconUrl: string;
}
export default class Client extends BaseClient {
private beaconUrl: string;
private blockCache = new NodeCache({ stdTTL: 60 * 60 * 12 });
private blockHashCache = new NodeCache({ stdTTL: 60 * 60 * 12 });
constructor(config: Config) {
super(config);
if (!config.beaconUrl) {
throw new Error("beaconUrl required");
}
this.beaconUrl = config.beaconUrl;
this.http.defaults.baseURL = this.beaconUrl;
}
private http: AxiosInstance = consensusClient;
async syncProver(
startPeriod: number,
currentPeriod: number,
startCommittee: Uint8Array[],
): Promise<{ syncCommittee: Uint8Array[]; period: number }> {
for (let period = startPeriod; period < currentPeriod; period += 1) {
try {
const update = await this.options.prover.getSyncUpdate(
period,
currentPeriod,
DEFAULT_BATCH_SIZE,
);
const validOrCommittee = await this.syncUpdateVerifyGetCommittee(
startCommittee,
period,
update,
);
if (!(validOrCommittee as boolean)) {
console.log(`Found invalid update at period(${period})`);
return {
syncCommittee: startCommittee,
period,
};
}
await this.options.store?.addUpdate(period, update);
startCommittee = validOrCommittee as Uint8Array[];
} catch (e) {
console.error(`failed to fetch sync update for period(${period})`);
return {
syncCommittee: startCommittee,
period,
};
}
}
return {
syncCommittee: startCommittee,
period: currentPeriod,
};
}
protected async getLatestExecution(): Promise<ExecutionInfo | null> {
const resp = await axios.get(
`/eth/v1/beacon/light_client/optimistic_update`,
);
const updateJSON = resp.data;
if (!updateJSON) {
throw Error(`fetching optimistic update failed`);
}
const update = optimisticUpdateFromJSON(updateJSON.data);
const verify = await optimisticUpdateVerify(
this.latestCommittee as Uint8Array[],
update,
);
if (!verify.correct) {
// @ts-ignore
console.error(`Invalid Optimistic Update: ${verify?.reason}`);
return null;
}
return this.getExecutionFromBlockRoot(
updateJSON.data.attested_header.beacon.slot,
updateJSON.data.attested_header.beacon.body_root,
);
}
protected async syncFromGenesis(): Promise<Uint8Array[]> {
return this.syncFromLastUpdate(this.genesisPeriod);
}
protected async syncFromLastUpdate(
startPeriod = this.latestPeriod,
): Promise<Uint8Array[]> {
const currentPeriod = this.getCurrentPeriod();
let startCommittee = this.genesisCommittee;
console.debug(
`Sync started from period(${startPeriod}) to period(${currentPeriod})`,
);
const { syncCommittee, period } = await this.syncProver(
startPeriod,
currentPeriod,
startCommittee,
);
if (period === currentPeriod) {
console.debug(
`Sync completed from period(${startPeriod}) to period(${currentPeriod})`,
);
return syncCommittee;
}
throw new Error("no honest prover found");
}
protected async syncUpdateVerifyGetCommittee(
prevCommittee: Uint8Array[],
period: number,
update: LightClientUpdate,
): Promise<false | Uint8Array[]> {
const updatePeriod = computeSyncPeriodAtSlot(
update.attestedHeader.beacon.slot,
);
if (period !== updatePeriod) {
console.error(
`Expected update with period ${period}, but received ${updatePeriod}`,
);
return false;
}
const prevCommitteeFast = deserializeSyncCommittee({
pubkeys: prevCommittee,
aggregatePubkey: bls.PublicKey.aggregate(
deserializePubkeys(prevCommittee),
).toBytes(),
});
try {
// check if the update has valid signatures
await assertValidLightClientUpdate(
this.config.chainConfig,
prevCommitteeFast,
update,
);
return update.nextSyncCommittee.pubkeys;
} catch (e) {
console.error(e);
return false;
}
}
private async getExecutionFromBlockRoot(
slot: bigint,
expectedBlockRoot: Bytes32,
): Promise<ExecutionInfo> {
const res = await axios.get(`/eth/v2/beacon/blocks/${slot}`);
if (!res.data) {
throw Error(`fetching block failed`);
}
const block = capella.ssz.BeaconBlock.fromJson(res.data.message);
const blockRoot = toHexString(
capella.ssz.BeaconBlockBody.hashTreeRoot(block.body),
);
if (blockRoot !== expectedBlockRoot) {
throw Error(
`block provided by the beacon chain api doesn't match the expected block root`,
);
}
this.blockCache.set<any>(Number(slot), block);
this.blockHashCache.set<Bytes32>(Number(slot), expectedBlockRoot);
return {
blockHash: toHexString(block.body.executionPayload.blockHash),
blockNumber: block.body.executionPayload.blockNumber,
};
}
}

13
src/node/index.ts Normal file
View File

@ -0,0 +1,13 @@
import Client from "./client.js";
import Prover from "./prover.js";
import Store from "./store.js";
function createDefaultClient(beaconUrl: string): Client {
return new Client({
store: new Store(),
prover: new Prover(),
beaconUrl,
});
}
export { Client, Prover, Store, createDefaultClient };

37
src/node/prover.ts Normal file
View File

@ -0,0 +1,37 @@
import { IProver } from "#interfaces.js";
import { LightClientUpdate } from "#types.js";
import { consensusClient } from "#util.js";
import { AxiosInstance } from "axios";
import * as capella from "@lodestar/types/capella";
export default class Prover implements IProver {
cachedSyncUpdate: Map<number, LightClientUpdate> = new Map();
private http: AxiosInstance = consensusClient;
async _getSyncUpdates(
startPeriod: number,
maxCount: number,
): Promise<LightClientUpdate[]> {
const res = await this.http(
`/eth/v1/beacon/light_client/updates?start_period=${startPeriod}&count=${maxCount}`,
);
return res.data.map((u: any) =>
capella.ssz.LightClientUpdate.fromJson(u.data),
);
}
async getSyncUpdate(
period: number,
currentPeriod: number,
cacheCount: number,
): Promise<LightClientUpdate> {
const _cacheCount = Math.min(currentPeriod - period + 1, cacheCount);
if (!this.cachedSyncUpdate.has(period)) {
const vals = await this._getSyncUpdates(period, _cacheCount);
for (let i = 0; i < _cacheCount; i++) {
this.cachedSyncUpdate.set(period + i, vals[i]);
}
}
return this.cachedSyncUpdate.get(period)!;
}
}

57
src/node/store.ts Normal file
View File

@ -0,0 +1,57 @@
import { digest } from "@chainsafe/as-sha256";
import { CommitteeSSZ, HashesSSZ } from "#ssz.js";
import { IStore } from "#interfaces.js";
import { concatBytes } from "@noble/hashes/utils";
import { LightClientUpdate } from "#types.js";
import * as capella from "@lodestar/types/capella";
export default class Store implements IStore {
store: {
[period: number]: {
update: Uint8Array;
nextCommittee: Uint8Array;
nextCommitteeHash: Uint8Array;
};
} = {};
async addUpdate(period: number, update: LightClientUpdate) {
try {
this.store[period] = {
update: capella.ssz.LightClientUpdate.serialize(update),
nextCommittee: CommitteeSSZ.serialize(update.nextSyncCommittee.pubkeys),
nextCommitteeHash: digest(
concatBytes(...update.nextSyncCommittee.pubkeys),
),
};
} catch (e) {
console.log(e);
}
}
getUpdate(period: number): Uint8Array {
if (period in this.store) return this.store[period].update;
throw new Error(`update unavailable for period ${period}`);
}
getCommittee(period: number): Uint8Array {
if (period < 1)
throw new Error("committee not unavailable for period less than 1");
const predPeriod = period - 1;
if (predPeriod in this.store) return this.store[predPeriod].nextCommittee;
throw new Error(`committee unavailable for period ${predPeriod}`);
}
getCommitteeHashes(period: number, count: number): Uint8Array {
if (period < 1)
throw new Error("committee not unavailable for period less than 1");
const predPeriod = period - 1;
const hashes = new Array(count).fill(0).map((_, i) => {
const p = predPeriod + i;
if (p in this.store) return this.store[p].nextCommitteeHash;
throw new Error(`committee unavailable for period ${p}`);
});
return HashesSSZ.serialize(hashes);
}
}

23
src/ssz.ts Normal file
View File

@ -0,0 +1,23 @@
import {
ByteVectorType,
ListCompositeType,
VectorCompositeType,
} from "@chainsafe/ssz";
import * as capella from "@lodestar/types/capella";
import { BEACON_SYNC_COMMITTEE_SIZE } from "./constants.js";
const MAX_BATCHSIZE = 10000;
//@ts-ignore
export const LightClientUpdatesSSZ = new ListCompositeType(
capella.ssz.LightClientUpdate as any,
MAX_BATCHSIZE,
);
export const CommitteeSSZ = new VectorCompositeType(
new ByteVectorType(48),
BEACON_SYNC_COMMITTEE_SIZE,
);
const HashSSZ = new ByteVectorType(32);
export const HashesSSZ = new ListCompositeType(HashSSZ, MAX_BATCHSIZE);

20
src/types.ts Normal file
View File

@ -0,0 +1,20 @@
import * as capella from "@lodestar/types/capella";
import * as phase0 from "@lodestar/types/phase0";
export type PubKeyString = string;
export type Slot = number;
export type Bytes32 = string;
export type LightClientUpdate = capella.LightClientUpdate;
export type OptimisticUpdate = capella.LightClientOptimisticUpdate;
export type GenesisData = {
committee: PubKeyString[];
slot: Slot;
time: number;
};
export type VerifyWithReason =
| { correct: true }
| { correct: false; reason: string };
export { capella, phase0 };

79
src/util.ts Normal file
View File

@ -0,0 +1,79 @@
import { createBeaconConfig } from "@lodestar/config";
import { BEACON_SYNC_SUPER_MAJORITY, mainnetConfig } from "./constants.js";
import { networksChainConfig } from "@lodestar/config/networks";
import { fromHexString } from "@chainsafe/ssz";
import { capella, OptimisticUpdate, phase0, VerifyWithReason } from "#types.js";
import { assertValidSignedHeader } from "@lodestar/light-client/validation";
import bls from "@chainsafe/bls/switchable";
import axiosRetry from "axios-retry";
import axios from "axios";
import { digest } from "@chainsafe/as-sha256";
import { concatBytes } from "@noble/hashes/utils";
export function getDefaultClientConfig() {
const chainConfig = createBeaconConfig(
networksChainConfig.mainnet,
fromHexString(mainnetConfig.genesis_validator_root),
);
return {
genesis: {
committee: mainnetConfig.committee_pk,
slot: parseInt(mainnetConfig.slot),
time: parseInt(mainnetConfig.genesis_time),
},
chainConfig,
n: 1,
};
}
export function optimisticUpdateFromJSON(update: any): OptimisticUpdate {
return capella.ssz.LightClientOptimisticUpdate.fromJson(update);
}
export async function optimisticUpdateVerify(
committee: Uint8Array[],
update: OptimisticUpdate,
): Promise<VerifyWithReason> {
try {
const { attestedHeader: header, syncAggregate } = update;
const headerBlockRoot = phase0.ssz.BeaconBlockHeader.hashTreeRoot(
header.beacon,
);
const committeeFast = this.deserializeSyncCommittee(committee);
try {
assertValidSignedHeader(
this.config.chainConfig,
committeeFast,
syncAggregate,
headerBlockRoot,
header.beacon.slot,
);
} catch (e) {
return { correct: false, reason: "invalid signatures" };
}
const participation =
syncAggregate.syncCommitteeBits.getTrueBitIndexes().length;
if (participation < BEACON_SYNC_SUPER_MAJORITY) {
return { correct: false, reason: "insufficient signatures" };
}
if (!this.isValidLightClientHeader(this.config.chainConfig, header)) {
return { correct: false, reason: "invalid header" };
}
return { correct: true };
} catch (e) {
console.error(e);
return { correct: false, reason: (e as Error).message };
}
}
export function deserializePubkeys(pubkeys) {
return pubkeys.map((pk) => bls.PublicKey.fromBytes(pk));
}
export function getCommitteeHash(committee: Uint8Array[]): Uint8Array {
return digest(concatBytes(...committee));
}
export const consensusClient = axios.create();
axiosRetry(consensusClient, { retries: 3 });

60
tsconfig.json Normal file
View File

@ -0,0 +1,60 @@
{
"compilerOptions": {
"outDir": "lib",
"module": "ESNext",
"target": "ESNext",
"declaration": true,
"inlineSourceMap": true,
"noEmit": true,
"noImplicitUseStrict": false,
"preserveConstEnums": true,
"removeComments": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"isolatedModules": false,
"alwaysStrict": true,
"noImplicitAny": false,
"noImplicitThis": false,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": false,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"#*": [
"src/*"
],
"*": [
"*",
"types/*"
]
},
"lib": [
"ES2020",
"ES2021",
"dom"
]
},
"include": [
"**/*.json",
"**/*.ts",
"**/*.cts",
"**/*.mts"
],
"exclude": [
"**/node_modules",
"lib"
]
}