*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 NodeCache from "node-cache";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { VerifyingProvider } from "./rpc/index.js";
|
||||
|
||||
export default class Client {
|
||||
latestCommittee?: Uint8Array[];
|
||||
|
@ -46,13 +47,25 @@ export default class Client {
|
|||
private boot = false;
|
||||
private beaconChainAPIURL: string;
|
||||
private store: IStore;
|
||||
|
||||
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.store = store;
|
||||
this.beaconChainAPIURL = beaconUrl;
|
||||
this.rpcUrl = rpcUrl;
|
||||
}
|
||||
|
||||
private _provider?: VerifyingProvider;
|
||||
|
||||
get provider(): VerifyingProvider {
|
||||
return this._provider;
|
||||
}
|
||||
|
||||
private _latestPeriod: number = -1;
|
||||
|
@ -82,10 +95,21 @@ export default class Client {
|
|||
|
||||
await this._sync();
|
||||
|
||||
if (!this.boot) {
|
||||
this.subscribe();
|
||||
this.boot = true;
|
||||
if (!this._provider) {
|
||||
const { blockhash, blockNumber } = await this.getNextValidExecutionInfo();
|
||||
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(
|
||||
|
@ -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 {
|
||||
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._blockHashCache.set(slot as any, expectedBlockRoot);
|
||||
|
||||
|
@ -235,7 +247,11 @@ export default class Client {
|
|||
|
||||
const currentPeriod = this.getCurrentPeriod();
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -263,6 +279,24 @@ export default class Client {
|
|||
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(
|
||||
prevCommittee: Uint8Array[],
|
||||
period: number,
|
||||
|
@ -311,9 +345,6 @@ export default class Client {
|
|||
console.error(`Invalid Optimistic Update: ${verify?.reason}`);
|
||||
return null;
|
||||
}
|
||||
console.log(
|
||||
`Optimistic update verified for slot ${updateJSON.data.attested_header.beacon.slot}`
|
||||
);
|
||||
return this.getExecutionFromBlockRoot(
|
||||
updateJSON.data.attested_header.beacon.slot,
|
||||
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 fetch, { Request, RequestInit } from "node-fetch";
|
||||
import NodeCache from "node-cache";
|
||||
import stringify from "json-stringify-deterministic";
|
||||
import { Client, Prover } from "./client/index.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 =
|
||||
"https://g.w.lavanet.xyz:443/gateway/eth/rpc-http/f195d68175eb091ec1f71d00f8952b85";
|
||||
|
@ -76,8 +79,10 @@ const plugin: Plugin = {
|
|||
async plugin(api: PluginAPI): Promise<void> {
|
||||
const prover = new Prover(CONSENSUS_RPC_URL);
|
||||
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();
|
||||
client.provider.rpc.pluginApi = api;
|
||||
const provider = client.provider;
|
||||
|
||||
api.registerMethod("consensus_committee_hashes", {
|
||||
cacheable: false,
|
||||
|
@ -160,32 +165,21 @@ const plugin: Plugin = {
|
|||
api.registerMethod("execution_request", {
|
||||
cacheable: false,
|
||||
async handler(request: ExecutionRequest): Promise<object> {
|
||||
const tempRequest = {
|
||||
method: request.method,
|
||||
...request.params,
|
||||
};
|
||||
const hash = api.util.crypto
|
||||
.createHash(stringify(tempRequest))
|
||||
.toString("hex");
|
||||
const cache = provider.rpc.getCachedRequest(request);
|
||||
|
||||
if (RPC_CACHE.has(hash)) {
|
||||
RPC_CACHE.ttl(hash);
|
||||
return RPC_CACHE.get(hash);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
try {
|
||||
let resp = await doFetch(EXECUTION_RPC_URL, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
if (resp && resp.result) {
|
||||
RPC_CACHE.set(hash, resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
} catch (e) {
|
||||
return e;
|
||||
if (provider.rpcMethodSupported(request.method)) {
|
||||
await provider.rpcMethod(request.method, request.params);
|
||||
} else {
|
||||
await provider.rpc.request(request);
|
||||
}
|
||||
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;
|
||||
|
||||
if (BigInt(block) > BigInt(client.latestPeriod)) {
|
||||
if (
|
||||
BigInt(block) > BigInt(client.latestPeriod) ||
|
||||
!client.blockHashCache.has(request.block)
|
||||
) {
|
||||
await client.sync();
|
||||
}
|
||||
|
||||
if (!client.blockHashCache.has(request.block)) {
|
||||
throw new Error("block not found");
|
||||
if (
|
||||
!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)) {
|
||||
|
@ -225,12 +237,12 @@ const plugin: Plugin = {
|
|||
return client.blockCache.get(request.block);
|
||||
}
|
||||
|
||||
await client.getExecutionFromBlockRoot(
|
||||
request.block as any,
|
||||
client.blockHashCache.get(request.block)
|
||||
const ret = await handleGETRequest(
|
||||
`${CONSENSUS_RPC_URL}/eth/v2/beacon/blocks/${request.block}`
|
||||
);
|
||||
client.blockCache.set(request.block, ret);
|
||||
|
||||
return client.blockCache.get(request.block);
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue