diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index 5db2f5e3..bc5241b0 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -303,7 +303,11 @@ export class ApiApp { RpcErrorCodes.MISSING_AUTH_HEADERS ); } - result = await this.ethApi.getUserOperationReceipt(params[0], sdAuthHeaders); + result = await this.ethApi.getUserOperationReceipt( + params[0], + sdAuthHeaders, + { id, jsonrpc } + ); break; case BundlerRPCMethods.eth_getUserOperationByHash: if (!sdAuthHeaders || !isSDAuthHeaders(sdAuthHeaders)) { @@ -312,7 +316,11 @@ export class ApiApp { RpcErrorCodes.MISSING_AUTH_HEADERS ); } - result = await this.ethApi.getUserOperationByHash(params[0], sdAuthHeaders); + result = await this.ethApi.getUserOperationByHash( + params[0], + sdAuthHeaders, + { id, jsonrpc } + ); break; case BundlerRPCMethods.web3_clientVersion: result = this.web3Api.clientVersion(); diff --git a/packages/api/src/modules/eth.ts b/packages/api/src/modules/eth.ts index 5cd8c327..98e60a4a 100644 --- a/packages/api/src/modules/eth.ts +++ b/packages/api/src/modules/eth.ts @@ -1,6 +1,7 @@ import { Eth } from "@skandha/executor/lib/modules/eth"; import { EstimatedUserOperationGas, + RpcRequestMetadata, SDAuthHeaders, UserOperationByHashResponse, UserOperationReceipt, @@ -50,26 +51,32 @@ export class EthAPI { /** * * @param hash user op hash + * @param sdAuthHeaders Silent Data authentication headers + * @param requestMetadata Original RPC request metadata (id and jsonrpc version) * @returns null in case the UserOperation is not yet included in a block, or a full UserOperation, * with the addition of entryPoint, blockNumber, blockHash and transactionHash */ async getUserOperationByHash( hash: string, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise { - return await this.ethModule.getUserOperationByHash(hash, sdAuthHeaders); + return await this.ethModule.getUserOperationByHash(hash, sdAuthHeaders, requestMetadata); } /** * * @param hash user op hash + * @param sdAuthHeaders Silent Data authentication headers + * @param requestMetadata Original RPC request metadata (id and jsonrpc version) * @returns a UserOperation receipt */ async getUserOperationReceipt( hash: string, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise { - return await this.ethModule.getUserOperationReceipt(hash, sdAuthHeaders); + return await this.ethModule.getUserOperationReceipt(hash, sdAuthHeaders, requestMetadata); } /** diff --git a/packages/executor/src/modules/eth.ts b/packages/executor/src/modules/eth.ts index 249a33ef..5028e03d 100644 --- a/packages/executor/src/modules/eth.ts +++ b/packages/executor/src/modules/eth.ts @@ -2,6 +2,7 @@ import RpcError from "@skandha/types/lib/api/errors/rpc-error"; import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; import { EstimatedUserOperationGas, + RpcRequestMetadata, SDAuthHeaders, UserOperationByHashResponse, UserOperationReceipt, @@ -481,6 +482,7 @@ export class Eth { async getUserOperationByHash( hash: string, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise { // Silent Data Auth is checked in the chain RPC, mempool check not supported // const entry = await this.mempoolService.getEntryByHash(hash); @@ -502,7 +504,7 @@ export class Eth { // } // } const rpcUserOp = await this.entryPointService.getUserOperationByHash( - hash, sdAuthHeaders, + hash, sdAuthHeaders, requestMetadata, ); if (!rpcUserOp && this.blockscoutApi) { return await this.blockscoutApi.getUserOperationByHash(hash); @@ -513,14 +515,17 @@ export class Eth { /** * * @param hash user op hash + * @param sdAuthHeaders Silent Data authentication headers + * @param requestMetadata Original RPC request metadata (id and jsonrpc version) * @returns a UserOperation receipt */ async getUserOperationReceipt( hash: string, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise { const rpcUserOp = await this.entryPointService.getUserOperationReceipt( - hash, sdAuthHeaders, + hash, sdAuthHeaders, requestMetadata, ); if (!rpcUserOp && this.blockscoutApi) { return await this.blockscoutApi.getUserOperationReceipt(hash); diff --git a/packages/executor/src/services/EntryPointService/service.ts b/packages/executor/src/services/EntryPointService/service.ts index c5c7a2bb..f1bde7d8 100644 --- a/packages/executor/src/services/EntryPointService/service.ts +++ b/packages/executor/src/services/EntryPointService/service.ts @@ -2,6 +2,7 @@ import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; import { IDbController, Logger } from "@skandha/types/lib"; import { + RpcRequestMetadata, SDAuthHeaders, UserOperationByHashResponse, UserOperationReceipt, @@ -42,6 +43,7 @@ export class EntryPointService { async getUserOperationByHash( userOpHash: string, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise { if (!userOpHash) { throw new RpcError( @@ -51,7 +53,7 @@ export class EntryPointService { } for (const [_, entryPoint] of Object.entries(this.entryPoints)) { try { - const res = entryPoint.getUserOperationByHash(userOpHash, sdAuthHeaders); + const res = entryPoint.getUserOperationByHash(userOpHash, sdAuthHeaders, requestMetadata); if (res) return res; } catch (err) { /* empty */ @@ -63,6 +65,7 @@ export class EntryPointService { async getUserOperationReceipt( userOpHash: string, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise { if (!userOpHash) { throw new RpcError( @@ -72,7 +75,7 @@ export class EntryPointService { } for (const [_, entryPoint] of Object.entries(this.entryPoints)) { try { - const res = entryPoint.getUserOperationReceipt(userOpHash, sdAuthHeaders); + const res = entryPoint.getUserOperationReceipt(userOpHash, sdAuthHeaders, requestMetadata); if (res) return res; } catch (err) { /* empty */ diff --git a/packages/executor/src/services/EntryPointService/versions/0.0.7.ts b/packages/executor/src/services/EntryPointService/versions/0.0.7.ts index 1a6d88c0..7f9466eb 100644 --- a/packages/executor/src/services/EntryPointService/versions/0.0.7.ts +++ b/packages/executor/src/services/EntryPointService/versions/0.0.7.ts @@ -14,6 +14,7 @@ import { IEntryPointSimulations__factory } from "@skandha/types/lib/contracts/EP import { hexlify, arrayify } from "ethers/lib/utils"; import { Logger } from "@skandha/types/lib"; import { + RpcRequestMetadata, UserOperationReceipt, UserOperationByHashResponse, SDAuthHeaders, @@ -59,6 +60,8 @@ import { toHex, createPublicClient, http, + toEventHash, + decodeEventLog, } from "viem"; import { USER_OPERATION_EVENT_HASH, UserOperationEventAbi } from '@skandha/types/lib/executor/abis' import { isPrivateEventLog, unwrapPrivateEvent } from '../../../utils/unwrapPrivateEvent' @@ -252,6 +255,7 @@ export class EntryPointV7Service implements IEntryPointService { async getUserOperationEvent( userOpHash: Hex, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ) { const { 'x-from-block': headerFromBlock, @@ -259,25 +263,33 @@ export class EntryPointV7Service implements IEntryPointService { } = sdAuthHeaders try { + // Validate x-from-block header is present (required for signature verification) + if (!headerFromBlock) { + throw new RpcError( + "Missing x-from-block header. This header is required for signature verification of eth_getLogs requests.", + RpcErrorCodes.INVALID_REQUEST + ); + } + + const fromBlock = BigInt(headerFromBlock); + const blockNumber = await this.publicClient.getBlockNumber(); - let minBlockNumber = blockNumber - BigInt(this.networkConfig.receiptLookupRange); - // underflow check - if (minBlockNumber < 0) { - minBlockNumber = BigInt(0); + const calculatedMin = blockNumber - BigInt(this.networkConfig.receiptLookupRange); + const minBlockNumber = calculatedMin < BigInt(0) ? BigInt(0) : calculatedMin; + + // Validate fromBlock is within the allowed range + if (fromBlock > blockNumber) { + throw new RpcError( + `x-from-block (${fromBlock}) cannot be greater than current block number (${blockNumber})`, + RpcErrorCodes.INVALID_REQUEST + ); } - let fromBlock = headerFromBlock - ? BigInt(headerFromBlock) - : undefined - - // ensure value is within the receipt lookup range - if ( - fromBlock === undefined || - fromBlock > blockNumber || - fromBlock < minBlockNumber - ) { - this.logger.warn(`fromBlock is out of range, using minBlockNumber: ${minBlockNumber}`) - fromBlock = minBlockNumber + if (fromBlock < minBlockNumber) { + throw new RpcError( + `x-from-block (${fromBlock}) is too old. Must be >= ${minBlockNumber} (current block ${blockNumber} - receiptLookupRange ${this.networkConfig.receiptLookupRange})`, + RpcErrorCodes.INVALID_REQUEST + ); } const clientHeaders = Object.fromEntries(Object @@ -293,15 +305,42 @@ export class EntryPointV7Service implements IEntryPointService { }), }) - const privateLogs = await clientWithHeaders.getLogs({ - address: this.address, - event: parseAbiItem([ - 'event PrivateEvent(address[] allowedViewers, bytes32 indexed eventType, bytes payload)', - ]), - fromBlock, - args: { - eventType: USER_OPERATION_EVENT_HASH, - } + // Manually construct eth_getLogs request to control the ID for signature verification + const privateEventAbi = parseAbiItem('event PrivateEvent(address[] allowedViewers, bytes32 indexed eventType, bytes payload)'); + const privateEventHash = toEventHash(privateEventAbi); + + const getLogsPayload = { + jsonrpc: requestMetadata.jsonrpc, + id: requestMetadata.id, + method: "eth_getLogs", + params: [{ + address: this.address, + fromBlock: toHex(fromBlock), + topics: [ + privateEventHash, + USER_OPERATION_EVENT_HASH + ] + }] + }; + + // Use transport.request to send the manually constructed payload + const response = await clientWithHeaders.transport.request({ + method: 'POST', + body: getLogsPayload + }) as { result?: any[] }; + + // Parse the logs from the response and decode the PrivateEvent + const privateLogs = (response.result || []).map((log: any) => { + const decoded = decodeEventLog({ + abi: [privateEventAbi], + data: log.data, + topics: log.topics + }); + return { + ...log, + args: decoded.args, + eventName: 'PrivateEvent' + }; }); for (const log of privateLogs) { @@ -335,8 +374,9 @@ export class EntryPointV7Service implements IEntryPointService { async getUserOperationReceipt( hash: Hex, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise { - const event = await this.getUserOperationEvent(hash, sdAuthHeaders); + const event = await this.getUserOperationEvent(hash, sdAuthHeaders, requestMetadata); if (!event) { return null; } @@ -358,8 +398,9 @@ export class EntryPointV7Service implements IEntryPointService { async getUserOperationByHash( hash: Hex, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise { - const event = await this.getUserOperationEvent(hash, sdAuthHeaders); + const event = await this.getUserOperationEvent(hash, sdAuthHeaders, requestMetadata); if (!event) { return null; } diff --git a/packages/executor/src/services/EntryPointService/versions/base.ts b/packages/executor/src/services/EntryPointService/versions/base.ts index fc39ef2e..890aa928 100644 --- a/packages/executor/src/services/EntryPointService/versions/base.ts +++ b/packages/executor/src/services/EntryPointService/versions/base.ts @@ -4,6 +4,7 @@ import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; import { IStakeManager } from "@skandha/types/lib/contracts/EPv7/core/StakeManager"; import { UserOperationEventEvent } from "@skandha/types/lib/contracts/EPv6/EntryPoint"; import { + RpcRequestMetadata, SDAuthHeaders, UserOperationByHashResponse, UserOperationReceipt, @@ -39,14 +40,17 @@ export interface IEntryPointService { getUserOperationEvent( userOpHash: string, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise; getUserOperationReceipt( hash: string, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise; getUserOperationByHash( hash: string, sdAuthHeaders: SDAuthHeaders, + requestMetadata: RpcRequestMetadata, ): Promise; encodeHandleOps(userOps: UserOperation[], beneficiary: string): Hex; diff --git a/packages/types/src/api/interfaces.ts b/packages/types/src/api/interfaces.ts index 8b4a59dd..01e6da19 100644 --- a/packages/types/src/api/interfaces.ts +++ b/packages/types/src/api/interfaces.ts @@ -175,3 +175,8 @@ export function isSDAuthHeaders( ) ) } + +export type RpcRequestMetadata = { + id: number + jsonrpc: string +}