*Add support for running verified calls with EVM
*Add caching of consensus blocks *Add caching of RPC calls
This commit is contained in:
parent
236338485b
commit
a12fd44e2b
|
@ -32,6 +32,7 @@ import * as bellatrix from "@lodestar/types/bellatrix";
|
||||||
import { init } from "@chainsafe/bls/switchable";
|
import { init } from "@chainsafe/bls/switchable";
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import { Mutex } from "async-mutex";
|
import { Mutex } from "async-mutex";
|
||||||
|
import { VerifyingProvider } from "./rpc/index.js";
|
||||||
|
|
||||||
export default class Client {
|
export default class Client {
|
||||||
latestCommittee?: Uint8Array[];
|
latestCommittee?: Uint8Array[];
|
||||||
|
@ -46,13 +47,25 @@ export default class Client {
|
||||||
private boot = false;
|
private boot = false;
|
||||||
private beaconChainAPIURL: string;
|
private beaconChainAPIURL: string;
|
||||||
private store: IStore;
|
private store: IStore;
|
||||||
|
|
||||||
private syncMutex = new Mutex();
|
private syncMutex = new Mutex();
|
||||||
|
private rpcUrl: string;
|
||||||
|
|
||||||
constructor(prover: IProver, store: IStore, beaconUrl: string) {
|
constructor(
|
||||||
|
prover: IProver,
|
||||||
|
store: IStore,
|
||||||
|
beaconUrl: string,
|
||||||
|
rpcUrl: string
|
||||||
|
) {
|
||||||
this.prover = prover;
|
this.prover = prover;
|
||||||
this.store = store;
|
this.store = store;
|
||||||
this.beaconChainAPIURL = beaconUrl;
|
this.beaconChainAPIURL = beaconUrl;
|
||||||
|
this.rpcUrl = rpcUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _provider?: VerifyingProvider;
|
||||||
|
|
||||||
|
get provider(): VerifyingProvider {
|
||||||
|
return this._provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _latestPeriod: number = -1;
|
private _latestPeriod: number = -1;
|
||||||
|
@ -82,10 +95,21 @@ export default class Client {
|
||||||
|
|
||||||
await this._sync();
|
await this._sync();
|
||||||
|
|
||||||
if (!this.boot) {
|
if (!this._provider) {
|
||||||
this.subscribe();
|
const { blockhash, blockNumber } = await this.getNextValidExecutionInfo();
|
||||||
this.boot = true;
|
const provider = new VerifyingProvider(
|
||||||
|
this.rpcUrl,
|
||||||
|
blockNumber,
|
||||||
|
blockhash
|
||||||
|
);
|
||||||
|
this._provider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ei = await this.getLatestExecution();
|
||||||
|
if (ei && ei.blockhash !== this.latestBlockHash) {
|
||||||
|
this.latestBlockHash = ei.blockhash;
|
||||||
|
}
|
||||||
|
this._provider.update(ei.blockhash, ei.blockNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncProver(
|
async syncProver(
|
||||||
|
@ -138,20 +162,6 @@ export default class Client {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async subscribe() {
|
|
||||||
setInterval(async () => {
|
|
||||||
try {
|
|
||||||
await this._sync();
|
|
||||||
const ei = await this.getLatestExecution();
|
|
||||||
if (ei && ei.blockhash !== this.latestBlockHash) {
|
|
||||||
this.latestBlockHash = ei.blockhash;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}, POLLING_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
optimisticUpdateFromJSON(update: any): OptimisticUpdate {
|
optimisticUpdateFromJSON(update: any): OptimisticUpdate {
|
||||||
return altair.ssz.LightClientOptimisticUpdate.fromJson(update);
|
return altair.ssz.LightClientOptimisticUpdate.fromJson(update);
|
||||||
}
|
}
|
||||||
|
@ -221,6 +231,8 @@ export default class Client {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`setting block ${slot} to cache`);
|
||||||
|
|
||||||
this._blockCache.set(slot as any, res);
|
this._blockCache.set(slot as any, res);
|
||||||
this._blockHashCache.set(slot as any, expectedBlockRoot);
|
this._blockHashCache.set(slot as any, expectedBlockRoot);
|
||||||
|
|
||||||
|
@ -235,7 +247,11 @@ export default class Client {
|
||||||
|
|
||||||
const currentPeriod = this.getCurrentPeriod();
|
const currentPeriod = this.getCurrentPeriod();
|
||||||
if (currentPeriod > this._latestPeriod) {
|
if (currentPeriod > this._latestPeriod) {
|
||||||
this.latestCommittee = await this.syncFromGenesis();
|
if (!this.boot) {
|
||||||
|
this.latestCommittee = await this.syncFromGenesis();
|
||||||
|
} else {
|
||||||
|
this.latestCommittee = await this.syncFromLastUpdate();
|
||||||
|
}
|
||||||
this._latestPeriod = currentPeriod;
|
this._latestPeriod = currentPeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,6 +279,24 @@ export default class Client {
|
||||||
throw new Error("no honest prover found");
|
throw new Error("no honest prover found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async syncFromLastUpdate() {
|
||||||
|
// get the tree size by currentPeriod - genesisPeriod
|
||||||
|
const currentPeriod = this.getCurrentPeriod();
|
||||||
|
let startPeriod = this.latestPeriod;
|
||||||
|
|
||||||
|
let startCommittee = this.latestCommittee;
|
||||||
|
|
||||||
|
const { syncCommittee, period } = await this.syncProver(
|
||||||
|
startPeriod,
|
||||||
|
currentPeriod,
|
||||||
|
startCommittee
|
||||||
|
);
|
||||||
|
if (period === currentPeriod) {
|
||||||
|
return syncCommittee;
|
||||||
|
}
|
||||||
|
throw new Error("no honest prover found");
|
||||||
|
}
|
||||||
|
|
||||||
protected async syncUpdateVerifyGetCommittee(
|
protected async syncUpdateVerifyGetCommittee(
|
||||||
prevCommittee: Uint8Array[],
|
prevCommittee: Uint8Array[],
|
||||||
period: number,
|
period: number,
|
||||||
|
@ -311,9 +345,6 @@ export default class Client {
|
||||||
console.error(`Invalid Optimistic Update: ${verify?.reason}`);
|
console.error(`Invalid Optimistic Update: ${verify?.reason}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
console.log(
|
|
||||||
`Optimistic update verified for slot ${updateJSON.data.attested_header.beacon.slot}`
|
|
||||||
);
|
|
||||||
return this.getExecutionFromBlockRoot(
|
return this.getExecutionFromBlockRoot(
|
||||||
updateJSON.data.attested_header.beacon.slot,
|
updateJSON.data.attested_header.beacon.slot,
|
||||||
updateJSON.data.attested_header.beacon.body_root
|
updateJSON.data.attested_header.beacon.body_root
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const ZERO_ADDR = '0x0000000000000000000000000000000000000000';
|
||||||
|
// TODO: set the correct gas limit!
|
||||||
|
export const GAS_LIMIT = '0x1c9c380';
|
||||||
|
export const REQUEST_BATCH_SIZE = 10;
|
||||||
|
export const MAX_SOCKET = 10;
|
||||||
|
export const EMPTY_ACCOUNT_EXTCODEHASH =
|
||||||
|
'0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
|
||||||
|
export const MAX_BLOCK_HISTORY = BigInt(256);
|
||||||
|
export const MAX_BLOCK_FUTURE = BigInt(3);
|
||||||
|
export const DEFAULT_BLOCK_PARAMETER = 'latest';
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { JSONRPCErrorCode, JSONRPCErrorException } from 'json-rpc-2.0';
|
||||||
|
|
||||||
|
export class InternalError extends JSONRPCErrorException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message, JSONRPCErrorCode.InternalError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidParamsError extends JSONRPCErrorException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message, JSONRPCErrorCode.InvalidParams);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { VerifyingProvider } from "./provider.js";
|
|
@ -0,0 +1,708 @@
|
||||||
|
import _ from "lodash";
|
||||||
|
import { Trie } from "@ethereumjs/trie";
|
||||||
|
import rlp from "rlp";
|
||||||
|
import { Common, Chain } from "@ethereumjs/common";
|
||||||
|
import {
|
||||||
|
Address,
|
||||||
|
Account,
|
||||||
|
toType,
|
||||||
|
bufferToHex,
|
||||||
|
toBuffer,
|
||||||
|
TypeOutput,
|
||||||
|
setLengthLeft,
|
||||||
|
KECCAK256_NULL_S,
|
||||||
|
} from "@ethereumjs/util";
|
||||||
|
import { VM } from "@ethereumjs/vm";
|
||||||
|
import { BlockHeader, Block } from "@ethereumjs/block";
|
||||||
|
import { Blockchain } from "@ethereumjs/blockchain";
|
||||||
|
import { TransactionFactory } from "@ethereumjs/tx";
|
||||||
|
import {
|
||||||
|
AddressHex,
|
||||||
|
Bytes32,
|
||||||
|
RPCTx,
|
||||||
|
AccountResponse,
|
||||||
|
CodeResponse,
|
||||||
|
Bytes,
|
||||||
|
BlockNumber as BlockOpt,
|
||||||
|
HexString,
|
||||||
|
JSONRPCReceipt,
|
||||||
|
AccessList,
|
||||||
|
GetProof,
|
||||||
|
} from "./types.js";
|
||||||
|
import {
|
||||||
|
ZERO_ADDR,
|
||||||
|
MAX_BLOCK_HISTORY,
|
||||||
|
MAX_BLOCK_FUTURE,
|
||||||
|
DEFAULT_BLOCK_PARAMETER,
|
||||||
|
} from "./constants.js";
|
||||||
|
import { headerDataFromWeb3Response, blockDataFromWeb3Response } from "./utils";
|
||||||
|
|
||||||
|
import { keccak256 } from "ethers";
|
||||||
|
import { InternalError, InvalidParamsError } from "./errors.js";
|
||||||
|
import { RPC } from "./rpc.js";
|
||||||
|
|
||||||
|
const bigIntToHex = (n: string | bigint | number): string =>
|
||||||
|
"0x" + BigInt(n).toString(16);
|
||||||
|
|
||||||
|
const emptyAccountSerialize = new Account().serialize();
|
||||||
|
export class VerifyingProvider {
|
||||||
|
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: bigint;
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
providerURL: string,
|
||||||
|
blockNumber: bigint | number,
|
||||||
|
blockHash: Bytes32,
|
||||||
|
chain: bigint | Chain = Chain.Mainnet
|
||||||
|
) {
|
||||||
|
this._rpc = new RPC({ URL: providerURL });
|
||||||
|
this.common = new Common({
|
||||||
|
chain,
|
||||||
|
});
|
||||||
|
const _blockNumber = BigInt(blockNumber);
|
||||||
|
this.latestBlockNumber = _blockNumber;
|
||||||
|
this.blockHashes[bigIntToHex(_blockNumber)] = blockHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rpc: RPC;
|
||||||
|
|
||||||
|
get rpc(): RPC {
|
||||||
|
return this._rpc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(blockHash: Bytes32, blockNumber: bigint) {
|
||||||
|
const blockNumberHex = bigIntToHex(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 = latestBlockNumber + BigInt(1); b <= blockNumber; b++) {
|
||||||
|
const bHex = bigIntToHex(b);
|
||||||
|
if (bHex in this.blockPromises) {
|
||||||
|
this.blockPromises[bHex].resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.getBlockHeader("latest");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
public rpcMethodSupported(method: string): boolean {
|
||||||
|
return this._methods.has(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getBalance(
|
||||||
|
addressHex: AddressHex,
|
||||||
|
blockOpt: BlockOpt = 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 InternalError(`RPC request failed`);
|
||||||
|
}
|
||||||
|
const isAccountCorrect = await this.verifyProof(
|
||||||
|
address,
|
||||||
|
[],
|
||||||
|
header.stateRoot,
|
||||||
|
proof
|
||||||
|
);
|
||||||
|
if (!isAccountCorrect) {
|
||||||
|
throw new InternalError("Invalid account proof provided by the RPC");
|
||||||
|
}
|
||||||
|
|
||||||
|
return bigIntToHex(proof.balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async blockNumber(): Promise<HexString> {
|
||||||
|
return bigIntToHex(this.latestBlockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async chainId(): Promise<HexString> {
|
||||||
|
return bigIntToHex(this.common.chainId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCode(
|
||||||
|
addressHex: AddressHex,
|
||||||
|
blockOpt: BlockOpt = 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 InternalError(`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 InternalError(`invalid account proof provided by the RPC`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCodeCorrect = await this.verifyCodeHash(
|
||||||
|
code,
|
||||||
|
accountProof.codeHash
|
||||||
|
);
|
||||||
|
if (!isCodeCorrect) {
|
||||||
|
throw new InternalError(
|
||||||
|
`code provided by the RPC doesn't match the account's codeHash`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTransactionCount(
|
||||||
|
addressHex: AddressHex,
|
||||||
|
blockOpt: BlockOpt = 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 InternalError(`RPC request failed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAccountCorrect = await this.verifyProof(
|
||||||
|
address,
|
||||||
|
[],
|
||||||
|
header.stateRoot,
|
||||||
|
proof
|
||||||
|
);
|
||||||
|
if (!isAccountCorrect) {
|
||||||
|
throw new InternalError(`invalid account proof provided by the RPC`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bigIntToHex(proof.nonce.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async call(
|
||||||
|
transaction: RPCTx,
|
||||||
|
blockOpt: BlockOpt = DEFAULT_BLOCK_PARAMETER
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.validateTx(transaction);
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new InvalidParamsError((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 InternalError(error.message.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async estimateGas(
|
||||||
|
transaction: RPCTx,
|
||||||
|
blockOpt: BlockOpt = DEFAULT_BLOCK_PARAMETER
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.validateTx(transaction);
|
||||||
|
} catch (e) {
|
||||||
|
throw new InvalidParamsError((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 InternalError(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 InternalError(`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 InternalError("the recipt provided by the RPC is invalid");
|
||||||
|
}
|
||||||
|
const tx = block.transactions[index];
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactionHash: txHash,
|
||||||
|
transactionIndex: bigIntToHex(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 InternalError(`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 InternalError(`RPC request failed`);
|
||||||
|
}
|
||||||
|
const responses = _.chunk(
|
||||||
|
rawResponse.map((r: any) => r.result),
|
||||||
|
2
|
||||||
|
) 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 InternalError(`invalid account proof provided by the RPC`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCodeCorrect = await this.verifyCodeHash(code, codeHash);
|
||||||
|
if (!isCodeCorrect) {
|
||||||
|
throw new InternalError(
|
||||||
|
`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: BlockOpt): Promise<BlockHeader> {
|
||||||
|
const blockNumber = this.getBlockNumberByBlockOpt(blockOpt);
|
||||||
|
await this.waitForBlockNumber(blockNumber);
|
||||||
|
const blockHash = await this.getBlockHash(blockNumber);
|
||||||
|
return this.getBlockHeaderByHash(blockHash);
|
||||||
|
}
|
||||||
|
private getBlockNumberByBlockOpt(blockOpt: BlockOpt): bigint {
|
||||||
|
// TODO: add support for blockOpts below
|
||||||
|
if (
|
||||||
|
typeof blockOpt === "string" &&
|
||||||
|
["pending", "earliest", "finalized", "safe"].includes(blockOpt)
|
||||||
|
) {
|
||||||
|
throw new InvalidParamsError(`"pending" is not yet supported`);
|
||||||
|
} else if (blockOpt === "latest") {
|
||||||
|
return this.latestBlockNumber;
|
||||||
|
} else {
|
||||||
|
const blockNumber = BigInt(blockOpt as any);
|
||||||
|
if (blockNumber > this.latestBlockNumber + MAX_BLOCK_FUTURE) {
|
||||||
|
throw new InvalidParamsError("specified block is too far in future");
|
||||||
|
} else if (blockNumber + MAX_BLOCK_HISTORY < this.latestBlockNumber) {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`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 InternalError(`RPC request failed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerData = headerDataFromWeb3Response(blockInfo);
|
||||||
|
const header = BlockHeader.fromHeaderData(headerData);
|
||||||
|
|
||||||
|
if (!header.hash().equals(toBuffer(blockHash))) {
|
||||||
|
throw new InternalError(
|
||||||
|
`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.toString());
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (let i = 0; i < storageKeys.length; i++) {
|
||||||
|
const sp = proof.storageProof[i];
|
||||||
|
const key = keccak256(
|
||||||
|
bufferToHex(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(code) === 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 InternalError(`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 InternalError(
|
||||||
|
`BN(${header.number}): blockhash doest match the blockData provided by the RPC`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await block.validateTransactionsTrie())) {
|
||||||
|
throw new InternalError(
|
||||||
|
`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(lastVerifiedBlockNumber)];
|
||||||
|
const header = await this.getBlockHeaderByHash(hash);
|
||||||
|
lastVerifiedBlockNumber--;
|
||||||
|
const parentBlockHash = bufferToHex(header.parentHash);
|
||||||
|
const parentBlockNumberHex = bigIntToHex(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)];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
import NodeCache from "node-cache";
|
||||||
|
import { PluginAPI } from "@lumeweb/interface-relay";
|
||||||
|
import stringify from "json-stringify-deterministic";
|
||||||
|
|
||||||
|
export type RPCRequest = {
|
||||||
|
method: string;
|
||||||
|
params: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RPCRequestRaw = RPCRequest & {
|
||||||
|
jsonrpc: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RPCResponse = {
|
||||||
|
success: boolean;
|
||||||
|
result: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProviderConfig = {
|
||||||
|
URL: string;
|
||||||
|
unsupportedMethods?: string[];
|
||||||
|
supportBatchRequests?: boolean;
|
||||||
|
batchSize?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RPC {
|
||||||
|
get cache(): NodeCache {
|
||||||
|
return this._cache;
|
||||||
|
}
|
||||||
|
private provider: ProviderConfig;
|
||||||
|
private _cache = new NodeCache({ stdTTL: 60 * 60 * 12 });
|
||||||
|
|
||||||
|
constructor(provider: ProviderConfig) {
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _pluginApi?: PluginAPI;
|
||||||
|
|
||||||
|
set pluginApi(value: PluginAPI) {
|
||||||
|
this._pluginApi = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(request: RPCRequest) {
|
||||||
|
if (this.provider.unsupportedMethods?.includes(request.method)) {
|
||||||
|
throw new Error("method not supported by the provider");
|
||||||
|
}
|
||||||
|
return await this._retryRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestBatch(requests: RPCRequest[]) {
|
||||||
|
if (
|
||||||
|
this.provider?.unsupportedMethods &&
|
||||||
|
requests.some((r) => this.provider.unsupportedMethods!.includes(r.method))
|
||||||
|
) {
|
||||||
|
throw new Error("method not supported by the provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = [];
|
||||||
|
for (const request of requests) {
|
||||||
|
const r = await this._retryRequest(request);
|
||||||
|
res.push(r);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _request(request: RPCRequestRaw): Promise<RPCResponse> {
|
||||||
|
try {
|
||||||
|
const response = await (
|
||||||
|
await fetch(this.provider.URL, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
})
|
||||||
|
).json();
|
||||||
|
|
||||||
|
if (response.result) {
|
||||||
|
const tempRequest = {
|
||||||
|
method: request.method,
|
||||||
|
params: request.params,
|
||||||
|
};
|
||||||
|
const hash = this._pluginApi.util.crypto
|
||||||
|
.createHash(stringify(tempRequest))
|
||||||
|
.toString("hex");
|
||||||
|
this._cache.set(hash, response);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: !response.error,
|
||||||
|
result: response.error || response.result,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
result: { message: `request failed: ${e}` },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCachedRequest(request: RPCRequest): RPCResponse | null {
|
||||||
|
const hash = this.hashRequest(request);
|
||||||
|
|
||||||
|
if (this.cache.has(hash)) {
|
||||||
|
this.cache.ttl(hash);
|
||||||
|
return this.cache.get(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hashRequest(request: RPCRequest): string {
|
||||||
|
const tempRequest = {
|
||||||
|
method: request.method,
|
||||||
|
params: request.params,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this._pluginApi.util.crypto
|
||||||
|
.createHash(stringify(tempRequest))
|
||||||
|
.toString("hex");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import type { GetProof } from "web3-eth";
|
||||||
|
import type { BlockNumber } from "web3-core";
|
||||||
|
import type { Method } from "web3-core-method";
|
||||||
|
import type { JsonTx } from "@ethereumjs/tx";
|
||||||
|
export type { GetProof, BlockNumber, Method };
|
||||||
|
|
||||||
|
export type Bytes = string;
|
||||||
|
export type Bytes32 = string;
|
||||||
|
export type AddressHex = string;
|
||||||
|
export type ChainId = number;
|
||||||
|
export type HexString = string;
|
||||||
|
|
||||||
|
// Some of the types below are taken from:
|
||||||
|
// https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/client/lib/rpc/modules/eth.ts
|
||||||
|
|
||||||
|
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 AccountResponse = GetProof;
|
||||||
|
export type CodeResponse = string;
|
||||||
|
|
||||||
|
export type JSONRPCReceipt = {
|
||||||
|
transactionHash: string; // DATA, 32 Bytes - hash of the transaction.
|
||||||
|
transactionIndex: string; // QUANTITY - integer of the transactions index position in the block.
|
||||||
|
blockHash: string; // DATA, 32 Bytes - hash of the block where this transaction was in.
|
||||||
|
blockNumber: string; // QUANTITY - block number where this transaction was in.
|
||||||
|
from: string; // DATA, 20 Bytes - address of the sender.
|
||||||
|
to: string | null; // DATA, 20 Bytes - address of the receiver. null when it's a contract creation transaction.
|
||||||
|
cumulativeGasUsed: string; // QUANTITY - The total amount of gas used when this transaction was executed in the block.
|
||||||
|
effectiveGasPrice: string; // QUANTITY - The final gas price per gas paid by the sender in wei.
|
||||||
|
gasUsed: string; // QUANTITY - The amount of gas used by this specific transaction alone.
|
||||||
|
contractAddress: string | null; // DATA, 20 Bytes - The contract address created, if the transaction was a contract creation, otherwise null.
|
||||||
|
logs: JSONRPCLog[]; // Array - Array of log objects, which this transaction generated.
|
||||||
|
logsBloom: string; // DATA, 256 Bytes - Bloom filter for light clients to quickly retrieve related logs.
|
||||||
|
// It also returns either:
|
||||||
|
root?: string; // DATA, 32 bytes of post-transaction stateroot (pre Byzantium)
|
||||||
|
status?: string; // QUANTITY, either 1 (success) or 0 (failure)
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JSONRPCLog = {
|
||||||
|
removed: boolean; // TAG - true when the log was removed, due to a chain reorganization. false if it's a valid log.
|
||||||
|
logIndex: string | null; // QUANTITY - integer of the log index position in the block. null when it's pending.
|
||||||
|
transactionIndex: string | null; // QUANTITY - integer of the transactions index position log was created from. null when it's pending.
|
||||||
|
transactionHash: string | null; // DATA, 32 Bytes - hash of the transactions this log was created from. null when it's pending.
|
||||||
|
blockHash: string | null; // DATA, 32 Bytes - hash of the block where this log was in. null when it's pending.
|
||||||
|
blockNumber: string | null; // QUANTITY - the block number where this log was in. null when it's pending.
|
||||||
|
address: string; // DATA, 20 Bytes - address from which this log originated.
|
||||||
|
data: string; // DATA - contains one or more 32 Bytes non-indexed arguments of the log.
|
||||||
|
topics: string[]; // Array of DATA - Array of 0 to 4 32 Bytes DATA of indexed log arguments.
|
||||||
|
// (In solidity: The first topic is the hash of the signature of the event
|
||||||
|
// (e.g. Deposit(address,bytes32,uint256)), except you declared the event with the anonymous specifier.)
|
||||||
|
};
|
|
@ -0,0 +1,68 @@
|
||||||
|
import {
|
||||||
|
setLengthLeft,
|
||||||
|
toBuffer,
|
||||||
|
bigIntToHex,
|
||||||
|
bufferToHex,
|
||||||
|
intToHex,
|
||||||
|
} from "@ethereumjs/util";
|
||||||
|
import { HeaderData, BlockData, Block } from "@ethereumjs/block";
|
||||||
|
import {
|
||||||
|
TxData,
|
||||||
|
AccessListEIP2930TxData,
|
||||||
|
FeeMarketEIP1559TxData,
|
||||||
|
TypedTransaction,
|
||||||
|
} 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),
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { InvalidParamsError } from './errors';
|
||||||
|
|
||||||
|
// Most of the validations are taken from:
|
||||||
|
// https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/client/lib/rpc/validation.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @memberof module:rpc
|
||||||
|
*/
|
||||||
|
export const validators = {
|
||||||
|
/**
|
||||||
|
* address validator to ensure has `0x` prefix and 20 bytes length
|
||||||
|
* @param params parameters of method
|
||||||
|
* @param index index of parameter
|
||||||
|
*/
|
||||||
|
address(params: any[], index: number) {
|
||||||
|
this.hex(params, index);
|
||||||
|
|
||||||
|
const address = params[index].substr(2);
|
||||||
|
|
||||||
|
if (!/^[0-9a-fA-F]+$/.test(address) || address.length !== 40) {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`invalid argument ${index}: invalid address`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hex validator to ensure has `0x` prefix
|
||||||
|
* @param params parameters of method
|
||||||
|
* @param index index of parameter
|
||||||
|
*/
|
||||||
|
hex(params: any[], index: number) {
|
||||||
|
if (typeof params[index] !== 'string') {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`invalid argument ${index}: argument must be a hex string`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params[index].substr(0, 2) !== '0x') {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`invalid argument ${index}: hex string without 0x prefix`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hex validator to validate block hash
|
||||||
|
* @param params parameters of method
|
||||||
|
* @param index index of parameter
|
||||||
|
*/
|
||||||
|
blockHash(params: any[], index: number) {
|
||||||
|
this.hex(params, index);
|
||||||
|
|
||||||
|
const blockHash = params[index].substring(2);
|
||||||
|
|
||||||
|
if (!/^[0-9a-fA-F]+$/.test(blockHash) || blockHash.length !== 64) {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`invalid argument ${index}: invalid block hash`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* validator to ensure valid block integer or hash, or string option ["latest", "earliest", "pending"]
|
||||||
|
* @param params parameters of method
|
||||||
|
* @param index index of parameter
|
||||||
|
*/
|
||||||
|
blockOption(params: any[], index: number) {
|
||||||
|
const blockOption = params[index];
|
||||||
|
|
||||||
|
if (typeof blockOption !== 'string') {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`invalid argument ${index}: argument must be a string`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (['latest', 'earliest', 'pending'].includes(blockOption)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this.hex([blockOption], 0);
|
||||||
|
} catch (e) {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`invalid argument ${index}: block option must be a valid hex block number, or "latest", "earliest" or "pending"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bool validator to check if type is boolean
|
||||||
|
* @param params parameters of method
|
||||||
|
* @param index index of parameter
|
||||||
|
*/
|
||||||
|
bool(params: any[], index: number) {
|
||||||
|
if (typeof params[index] !== 'boolean') {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`invalid argument ${index}: argument is not boolean`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* params length validator
|
||||||
|
* @param params parameters of method
|
||||||
|
* @requiredParamsLength required length of parameters
|
||||||
|
*/
|
||||||
|
paramsLength(
|
||||||
|
params: any[],
|
||||||
|
requiredParamsCount: number,
|
||||||
|
maxParamsCount: number = requiredParamsCount,
|
||||||
|
) {
|
||||||
|
if (params.length < requiredParamsCount || params.length > maxParamsCount) {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`missing value for required argument ${params.length}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
transaction(params: any[], index: number) {
|
||||||
|
const tx = params[index];
|
||||||
|
|
||||||
|
if (typeof tx !== 'object') {
|
||||||
|
throw new InvalidParamsError(
|
||||||
|
`invalid argument ${index}: argument must be an object`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate addresses
|
||||||
|
for (const field of [tx.to, tx.from]) {
|
||||||
|
// TODO: the below will create an error with incorrect index if the tx is not at index 0
|
||||||
|
if (field !== undefined) this.address([field], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate hex
|
||||||
|
for (const field of [tx.gas, tx.gasPrice, tx.value, tx.data]) {
|
||||||
|
// TODO: the below will create an error with incorrect index if the tx is not at index 0
|
||||||
|
if (field !== undefined) this.hex([field], 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
74
src/index.ts
74
src/index.ts
|
@ -1,9 +1,12 @@
|
||||||
import type { Plugin, PluginAPI } from "@lumeweb/interface-relay";
|
import type { Plugin, PluginAPI } from "@lumeweb/interface-relay";
|
||||||
import fetch, { Request, RequestInit } from "node-fetch";
|
import fetch, { Request, RequestInit } from "node-fetch";
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import stringify from "json-stringify-deterministic";
|
|
||||||
import { Client, Prover } from "./client/index.js";
|
import { Client, Prover } from "./client/index.js";
|
||||||
import { MemoryStore } from "./client/memory-store.js";
|
import { MemoryStore } from "./client/memory-store.js";
|
||||||
|
import { computeSyncPeriodAtSlot } from "@lodestar/light-client/utils";
|
||||||
|
import { toHexString } from "@chainsafe/ssz";
|
||||||
|
import { DEFAULT_BATCH_SIZE } from "./client/constants.js";
|
||||||
|
import { handleGETRequest } from "./client/utils.js";
|
||||||
|
|
||||||
const EXECUTION_RPC_URL =
|
const EXECUTION_RPC_URL =
|
||||||
"https://g.w.lavanet.xyz:443/gateway/eth/rpc-http/f195d68175eb091ec1f71d00f8952b85";
|
"https://g.w.lavanet.xyz:443/gateway/eth/rpc-http/f195d68175eb091ec1f71d00f8952b85";
|
||||||
|
@ -76,8 +79,10 @@ const plugin: Plugin = {
|
||||||
async plugin(api: PluginAPI): Promise<void> {
|
async plugin(api: PluginAPI): Promise<void> {
|
||||||
const prover = new Prover(CONSENSUS_RPC_URL);
|
const prover = new Prover(CONSENSUS_RPC_URL);
|
||||||
const store = new MemoryStore();
|
const store = new MemoryStore();
|
||||||
client = new Client(prover, store, CONSENSUS_RPC_URL);
|
client = new Client(prover, store, CONSENSUS_RPC_URL, EXECUTION_RPC_URL);
|
||||||
await client.sync();
|
await client.sync();
|
||||||
|
client.provider.rpc.pluginApi = api;
|
||||||
|
const provider = client.provider;
|
||||||
|
|
||||||
api.registerMethod("consensus_committee_hashes", {
|
api.registerMethod("consensus_committee_hashes", {
|
||||||
cacheable: false,
|
cacheable: false,
|
||||||
|
@ -160,32 +165,21 @@ const plugin: Plugin = {
|
||||||
api.registerMethod("execution_request", {
|
api.registerMethod("execution_request", {
|
||||||
cacheable: false,
|
cacheable: false,
|
||||||
async handler(request: ExecutionRequest): Promise<object> {
|
async handler(request: ExecutionRequest): Promise<object> {
|
||||||
const tempRequest = {
|
const cache = provider.rpc.getCachedRequest(request);
|
||||||
method: request.method,
|
|
||||||
...request.params,
|
|
||||||
};
|
|
||||||
const hash = api.util.crypto
|
|
||||||
.createHash(stringify(tempRequest))
|
|
||||||
.toString("hex");
|
|
||||||
|
|
||||||
if (RPC_CACHE.has(hash)) {
|
if (cache) {
|
||||||
RPC_CACHE.ttl(hash);
|
return cache;
|
||||||
return RPC_CACHE.get(hash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (provider.rpcMethodSupported(request.method)) {
|
||||||
let resp = await doFetch(EXECUTION_RPC_URL, {
|
await provider.rpcMethod(request.method, request.params);
|
||||||
method: "POST",
|
} else {
|
||||||
body: JSON.stringify(request),
|
await provider.rpc.request(request);
|
||||||
});
|
|
||||||
if (resp && resp.result) {
|
|
||||||
RPC_CACHE.set(hash, resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
} catch (e) {
|
|
||||||
return e;
|
|
||||||
}
|
}
|
||||||
|
let ret = provider.rpc.getCachedRequest(request);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return { ...ret, id: request.id ?? ret.id };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -211,12 +205,30 @@ const plugin: Plugin = {
|
||||||
|
|
||||||
const block = request?.block;
|
const block = request?.block;
|
||||||
|
|
||||||
if (BigInt(block) > BigInt(client.latestPeriod)) {
|
if (
|
||||||
|
BigInt(block) > BigInt(client.latestPeriod) ||
|
||||||
|
!client.blockHashCache.has(request.block)
|
||||||
|
) {
|
||||||
await client.sync();
|
await client.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!client.blockHashCache.has(request.block)) {
|
if (
|
||||||
throw new Error("block not found");
|
!client.blockHashCache.has(request.block) &&
|
||||||
|
!client.blockCache.has(request.block)
|
||||||
|
) {
|
||||||
|
let state;
|
||||||
|
try {
|
||||||
|
const period = computeSyncPeriodAtSlot(request.block);
|
||||||
|
state = await prover.getSyncUpdate(
|
||||||
|
period,
|
||||||
|
period,
|
||||||
|
DEFAULT_BATCH_SIZE
|
||||||
|
);
|
||||||
|
await client.getExecutionFromBlockRoot(
|
||||||
|
request.block as any,
|
||||||
|
toHexString(state.attestedHeader.beacon.bodyRoot)
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.blockCache.has(request.block)) {
|
if (client.blockCache.has(request.block)) {
|
||||||
|
@ -225,12 +237,12 @@ const plugin: Plugin = {
|
||||||
return client.blockCache.get(request.block);
|
return client.blockCache.get(request.block);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.getExecutionFromBlockRoot(
|
const ret = await handleGETRequest(
|
||||||
request.block as any,
|
`${CONSENSUS_RPC_URL}/eth/v2/beacon/blocks/${request.block}`
|
||||||
client.blockHashCache.get(request.block)
|
|
||||||
);
|
);
|
||||||
|
client.blockCache.set(request.block, ret);
|
||||||
|
|
||||||
return client.blockCache.get(request.block);
|
return ret;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue