For developers / JSON-RPC

Deviations from Ethereum

Live

Pickle is Ethereum-shaped, not Ethereum-equivalent. This is the complete list of places where a standard client will get an answer it did not expect, ordered by how much time each one costs to find out the hard way.

Revert reasons are undecodable

error.data is never populated

A revert arrives as message text - "execution reverted: 0x<returndata>" - and error.data is absent.

Libraries decode custom errors and revert strings out of error.data, so on this chain they cannot. ethers and viem will both surface the raw message instead of a decoded error. If you need the reason, parse the hex out of the message yourself and decode it against your own ABI.

If you need the reason, the return data is in the message text and you can decode it yourself:

javascript
try {
  await client.readContract({ /* … */ });
} catch (err) {
  // "execution reverted: 0x08c379a0…"
  const hex = String(err.message).match(/0x[0-9a-fA-F]+/)?.[0];
  // Decode that hex against your own ABI's errors, or against Error(string).
}

Read from sequencer/src/rpc.rs:1262

There is no historical state

eth_getBalance, eth_getTransactionCount, eth_getCode and eth_getStorageAt parse the address and then read the latest published snapshot. The block parameter is accepted and silently ignored - a request for a balance at block 100 does not error, it answers with the balance now.

eth_call and eth_estimateGas do the same, and additionally ignore the gas, gasPrice and nonce you send them: simulation always runs at the 30,000,000 block gas limit against latest state.

This one fails silently, which is why it is second

Any archive-style pattern - reading a historical balance, replaying state at a past block, computing a snapshot - returns confident, plausible, wrong answers here rather than an error. Read from the explorer instead for anything historical.

Block tags collapse

latest, pending, safe and finalized all resolve to the same thing: the retained tip. And earliest resolves to the earliest retained block, not block 0 - so it moves forward as the window slides.

EIP-1898 objects are accepted: { blockNumber } or { blockHash }, with a hash resolved through the index and answering null when it is no longer retained. Anything else is -32602 "block tag".

A client that distinguishes safe from finalized to decide when to trust a result gets no signal from this chain. Finality here is L1 settlement and is not visible through these tags at all.

Roots and blooms are zero

On both blocks and receipts, stateRoot, transactionsRoot and receiptsRoot are zero, and logsBloom is 512 zero characters.

Two consequences. A client that verifies roots cannot verify anything here. And a client that pre-filters by bloom before fetching logs will match nothing - filter with eth_getLogs, which reads a real index.

Headers are otherwise properly Cancun-shaped, including mixHash, withdrawalsRoot, blobGasUsed, excessBlobGas and parentBeaconBlockRoot. mixHash is load-bearing rather than decorative: without it every revm-based client, forge script included, rejects the header with prevrandao not set before simulating.

Receipt and transaction fields

  • type is always "0x0", whatever envelope you actually sent, and a transaction reports a single flat gasPrice - no maxFeePerGas, maxPriorityFeePerGas or accessList comes back. Typed transactions are accepted on the way in; the response shape simply does not reflect the type.
  • cumulativeGasUsed is that transaction's own gasUsed, not a running total for the block. Summing it across a block double-counts nothing but tells you nothing either.
  • blockHash and blockNumber are null between execution and the EVM seal. A receipt exists in that window, so "receipt is not null" is not the same test as "included in a block".
  • A transaction carries non-standard extras - raw, miniBlockNumber, miniBlockHash - and a receipt carries the latter two. They are useful and they are not portable.
  • eth_getBlockByNumber with fullTransactions: true can return a mixed array: objects for transactions whose receipts are still indexed, bare hash strings for those already evicted.
  • eth_getBlockReceipts silently omits evicted receipts, so the array can be shorter than the block's transaction list.

logIndex is per transaction

logIndex is enumerated over the logs of its own receipt, not over the block. Two logs in one block can both have logIndex 0.

If you key an event store on (blockNumber, logIndex) - the usual composite primary key for an indexer - you will collide. Include transactionHash.

Pending is not a mempool

There is no public mempool to be pending in. A hash reaches eth_newPendingTransactionFilter and the newPendingTransactions subscription only after the transaction has executed and been indexed.

So it is an executed-transaction feed under a misleading name. You cannot watch for incoming transactions before they land, and there is nothing to front-run through this interface.

Methods that answer with constants

Two methods look like they work and return values that are not measurements. Both are marked in the reference, and both are more dangerous than a method that errors:

  • eth_feeHistory reads only blockCount, ignores newestBlock and rewardPercentiles entirely, and returns baseFeePerGas as 0x1 repeated with gasUsedRatio as 0.0 repeated.
  • eth_createAccessList returns { accessList: [], gasUsed: "0x0" } without looking at the parameters or the state.

A third group answers honestly but emptily, because the concepts do not apply: eth_accounts is always empty, eth_mining false, eth_hashrate zero, and the four uncle methods return zero or null without validating their parameters.

Namespaces that do not exist

rpc_modules advertises eth, net, web3 and pickle. Absent and unimplemented: debug, trace, txpool and admin. So there is no debug_traceTransaction - for call traces, use the explorer, which has them.

The signing methods are registered and deliberately refuse: eth_sendTransaction, eth_sign, eth_signTransaction, personal_sign and eth_coinbase all answer -32601. The node holds no user keys - sign locally and use eth_sendRawTransaction.