Skip to content

Commit

Permalink
verify logs for eth_getLogs
Browse files Browse the repository at this point in the history
  • Loading branch information
rista404 committed Oct 10, 2024
1 parent c7cb2c1 commit 6606f1f
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 33 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@ethereumjs/util": "^9.0.3",
"@ethereumjs/vm": "^8.0.0",
"axios": "0.27.2",
"ethereum-bloom-filters": "^1.2.0",
"json-rpc-2.0": "1.4.1",
"kzg-wasm": "^0.4.0",
"lodash": "4.17.21",
Expand Down
7 changes: 7 additions & 0 deletions src/json-rpc-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { JSONRPCLogFilter, RPCTx } from './types';
import log from './logger';
import { VerifyingProvider } from './provider';
import { validators } from './validation';
import { InvalidParamsError } from './errors';

export function getJSONRPCServer(provider: VerifyingProvider) {
const server = new JSONRPCServer();
Expand Down Expand Up @@ -84,6 +85,12 @@ export function getJSONRPCServer(provider: VerifyingProvider) {
// Validate block options if they are present and blockHash is not
if (filter.fromBlock) validators.blockOption([filter.fromBlock], 0);
if (filter.toBlock) validators.blockOption([filter.toBlock], 0);
// Pending blocks cannot be verified in eth_getLogs
if ([filter.fromBlock, filter.toBlock].includes('pending')) {
throw new InvalidParamsError(
'eth_getLogs does not support pending blocks.',
);
}
}

// Validate address if it is present
Expand Down
193 changes: 163 additions & 30 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import {
KECCAK256_RLP_S,
KECCAK256_NULL_S,
equalsBytes,
KECCAK256_RLP,
} from '@ethereumjs/util';
import { VM } from '@ethereumjs/vm';
import { VM, encodeReceipt } from '@ethereumjs/vm';
import { BlockHeader, Block } from '@ethereumjs/block';
import { Blockchain } from '@ethereumjs/blockchain';
import { TransactionFactory } from '@ethereumjs/tx';
import { TransactionFactory, TransactionType } from '@ethereumjs/tx';
import { isInBloom, isTopicInBloom } from 'ethereum-bloom-filters';
import {
AddressHex,
Bytes32,
Expand Down Expand Up @@ -48,6 +50,7 @@ import {
headerDataFromWeb3Response,
blockDataFromWeb3Response,
toJSONRPCBlock,
txReceiptFromJSONRPCReceipt,
} from './utils';
import { RPC } from './rpc';

Expand Down Expand Up @@ -164,7 +167,6 @@ export class VerifyingProvider {
}

async getLogs(filter: JSONRPCLogFilter): Promise<JSONRPCLog[]> {
// naive, forward the request to the RPC
const res = await this.rpc.request({
method: 'eth_getLogs',
params: [filter],
Expand All @@ -173,45 +175,175 @@ export class VerifyingProvider {
throw new InternalError(`RPC request failed`);
}

// throw new InvalidParamsError(`"pending" is not yet supported`);

const logs = res.result as JSONRPCLog[];

// check logs against the state
const blockNumbers = new Set(
logs
.map(l => l.blockNumber)
.filter(bn => bn && Number.isInteger(parseInt(bn, 16))),
) as Set<string>;

// caches
// blockNumber -> blockHeader
const blockHeaders = new Map<string, BlockHeader>();
// blockNumber -> block
const blocks = new Map<string, Block>();
// blockHash -> receipts
const blockReceipts = new Map<string, JSONRPCReceipt[]>();

// fetch blocks
await Promise.all(
Array.from(blockNumbers).map(async blockNumber => {
if (!blockHeaders.has(blockNumber)) {
const header = await this.getBlockHeader(blockNumber);
blockHeaders.set(blockNumber, header);
}
if (!blocks.has(blockNumber)) {
const header = blockHeaders.get(blockNumber)!;
const block = await this.getBlock(header);
blocks.set(blockNumber, block);
}
}),
);

// TODO parallelize
for (const log of logs) {
// TODO handle pending logs
const header = await this.getBlockHeader(log.blockNumber);
for (const l of logs) {
if (
typeof l.logIndex !== 'string' ||
typeof l.blockNumber !== 'string' ||
typeof l.blockHash !== 'string' ||
typeof l.transactionHash !== 'string' ||
typeof l.transactionIndex !== 'string'
) {
throw new InternalError(`"pending" logs are not supported`);
}

// receiptTrie
// logsBloom
const block = await this.getBlock(header);
const block = blocks.get(l.blockNumber);
if (!block) {
throw new InternalError(`Block ${l.blockNumber} not found`);
}
const blockHash = bytesToHex(block.hash());

// verify block hash matches
if (blockHash !== l.blockHash.toLowerCase()) {
throw new InternalError(
'the log provided by the RPC is invalid: blockHash not matching',
);
}

// verify transaction
// log.transactionHash, log.transactionIndex
// verify transaction index and hash matches
const txIndex = block.transactions.findIndex(
tx => bytesToHex(tx.hash()) === log.transactionHash.toLowerCase(),
tx => bytesToHex(tx.hash()) === l.transactionHash!.toLowerCase(),
);
if (txIndex === -1 || txIndex !== log.transactionIndex) {
throw new InternalError('the recipt provided by the RPC is invalid');
if (txIndex === -1 || txIndex !== parseInt(l.transactionIndex, 16)) {
throw new InternalError(
'the log provided by the RPC is invalid: transactionHash not matching',
);
}

// check if the log is not in the logsBloom
const logsBloom = bytesToHex(block.header.logsBloom);
if (
!isInBloom(logsBloom, l.address) ||
l.topics.some(topic => !isTopicInBloom(logsBloom, topic))
) {
throw new InternalError('the log is not in the logsBloom');
}

// fetch all receipts in the block
let receipts: JSONRPCReceipt[];
if (blockReceipts.has(blockHash)) {
receipts = blockReceipts.get(blockHash)!;
} else {
receipts = await this.getBlockReceipts(blockHash);
blockReceipts.set(blockHash, receipts);
}
const tx = block.transactions[txIndex];
// log.logIndex

// eth_blockReceipts
// eth_getTransactionReceipt
// reconstruct receipt trie
const reconstructedReceiptTrie = new Trie();
for (let i = 0; i < receipts.length; i++) {
const receiptJson = receipts[i] as JSONRPCReceipt;
const receipt = txReceiptFromJSONRPCReceipt(receiptJson);
const type: TransactionType = parseInt(receiptJson.type, 16);
const encoded = encodeReceipt(receipt, type);
await reconstructedReceiptTrie.put(rlp.encode(i), encoded);
}

// TODO: to verify the params below download all the tx recipts
// of the block, compute the recipt root and verify the recipt
// root matches that in the blockHeader
// check if it matches
const computedReceiptRoot =
reconstructedReceiptTrie !== undefined
? bytesToHex(reconstructedReceiptTrie.root())
: KECCAK256_RLP;

// log.data, log.topics?
if (computedReceiptRoot !== bytesToHex(block.header.receiptTrie)) {
throw new InternalError(
'Receipt trie root does not match the block header receiptTrie',
);
}

// check log is included in the receipt
const receipt = receipts.find(
r =>
r.transactionHash.toLowerCase() === l.transactionHash!.toLowerCase(),
);
if (!receipt) {
throw new InternalError('Receipt not found for the log');
}
const logFound = receipt.logs.some(
log =>
log.address.toLowerCase() === l.address.toLowerCase() &&
log.data.toLowerCase() === l.data.toLowerCase() &&
log.topics.length === l.topics.length &&
log.topics.every(
(topic, index) =>
topic.toLowerCase() === l.topics[index].toLowerCase(),
),
);
if (!logFound) {
throw new InternalError('Log not found in the receipt');
}
}

return res.result;
}

// TODO expose as an RPC request (and verify)
async getBlockReceipts(blockHash: Bytes32): Promise<JSONRPCReceipt[]> {
try {
const { result: receipts, success } = await this.rpc.request({
method: 'eth_getBlockReceipts',
params: [blockHash],
});

if (success) {
return receipts;
} else {
throw new Error('eth_getBlockReceipts RPC request failed');
}
} catch (error) {
// only fallback to eth_getTransactionReceipt if the method is not supported
// otherwise fail
if (!error?.message.includes('method not supported')) {
throw error;
}

const header = await this.getBlockHeaderByHash(blockHash);
const block = await this.getBlock(header);

const receipts = await this.rpc.requestBatch(
block.transactions.map(tx => ({
method: 'eth_getTransactionReceipt',
params: [bytesToHex(tx.hash())],
})),
);

if (receipts.some(r => !r.success)) {
throw new InternalError(`eth_getTransactionReceipt RPC request failed`);
}

return receipts.map(r => r.result);
}
}

async getCode(
addressHex: AddressHex,
blockOpt: BlockOpt = DEFAULT_BLOCK_PARAMETER,
Expand Down Expand Up @@ -433,7 +565,7 @@ export class VerifyingProvider {
tx => bytesToHex(tx.hash()) === txHash.toLowerCase(),
);
if (index === -1) {
throw new InternalError('the recipt provided by the RPC is invalid');
throw new InternalError('the receipt provided by the RPC is invalid');
}
const tx = block.transactions[index];

Expand All @@ -444,8 +576,9 @@ export class VerifyingProvider {
blockNumber: bigIntToHex(block.header.number),
from: tx.getSenderAddress().toString(),
to: tx.to?.toString() ?? null,
// TODO: to verify the params below download all the tx recipts
// of the block, compute the recipt root and verify the recipt
type: bigIntToHex(tx.type),
// TODO: to verify the params below download all the tx receipts
// of the block, compute the receipt root and verify the receipt
// root matches that in the blockHeader
cumulativeGasUsed: '0x0',
effectiveGasPrice: '0x0',
Expand Down Expand Up @@ -546,7 +679,7 @@ export class VerifyingProvider {
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}`,
`specified block (${blockNumber}) cannot be older that ${MAX_BLOCK_HISTORY}`,
);
}
return blockNumber;
Expand Down
3 changes: 3 additions & 0 deletions src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export class RPC {
) {
throw new Error('method not supported by the provider');
}
log.debug(
`RPC Request: ${request.method}: ${JSON.stringify(request.params)}`,
);
return await this._retryRequest(request);
}

Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export type JSONRPCReceipt = {
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.
type: string; // QUANTITY - EIP-2718 Typed Transaction type
// It also returns either:
root?: string; // DATA, 32 bytes of post-transaction stateroot (pre Byzantium)
status?: string; // QUANTITY, either 1 (success) or 0 (failure)
Expand Down
45 changes: 44 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import {
FeeMarketEIP1559TxData,
TypedTransaction,
} from '@ethereumjs/tx';
import { JSONRPCTx, JSONRPCBlock } from './types';
import {
TxReceipt,
PreByzantiumTxReceipt,
PostByzantiumTxReceipt,
} from '@ethereumjs/vm';
import { Log } from '@ethereumjs/evm';
import { JSONRPCTx, JSONRPCBlock, JSONRPCReceipt } from './types';

const isTruthy = (val: any) => !!val;

Expand Down Expand Up @@ -138,3 +144,40 @@ export function toJSONRPCBlock(
baseFeePerGas: header.baseFeePerGas,
};
}

export function txReceiptFromJSONRPCReceipt(
receipt: JSONRPCReceipt,
): TxReceipt {
// Transform logs
const logs: Log[] = receipt.logs.map(log => [
hexToBytes(log.address),
log.topics.map(topic => hexToBytes(topic)),
hexToBytes(log.data),
]);

// Base receipt fields
const baseReceipt = {
cumulativeBlockGasUsed: BigInt(receipt.cumulativeGasUsed),
bitvector: hexToBytes(receipt.logsBloom),
logs,
};

// Determine the type of receipt
if (receipt.root) {
// Pre-Byzantium receipt
const preByzantiumReceipt: PreByzantiumTxReceipt = {
...baseReceipt,
stateRoot: hexToBytes(receipt.root),
};
return preByzantiumReceipt;
} else if (receipt.status !== undefined) {
// Post-Byzantium receipt
const postByzantiumReceipt: PostByzantiumTxReceipt = {
...baseReceipt,
status: parseInt(receipt.status, 16) as 0 | 1,
};
return postByzantiumReceipt;
} else {
throw new Error('Unsupported receipt type');
}
}
8 changes: 6 additions & 2 deletions src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const validators = {
}
},
/**
* validator to ensure valid block integer or hash, or string option ["latest", "earliest", "pending"]
* validator to ensure valid block integer or hash, or string option ["latest", "earliest", "pending", "finalized", "safe"]
* @param params parameters of method
* @param index index of parameter
*/
Expand All @@ -74,7 +74,11 @@ export const validators = {
}

try {
if (['latest', 'earliest', 'pending'].includes(blockOption)) {
if (
['latest', 'earliest', 'pending', 'finalized', 'safe'].includes(
blockOption,
)
) {
return;
}
return this.hex([blockOption], 0);
Expand Down
Loading

0 comments on commit 6606f1f

Please sign in to comment.