JSON-RPC API

Rustock serves an rskj-compatible JSON-RPC 2.0 interface over HTTP. Point any web3 library at it; the wire format and the error semantics are rskj's.

Endpoint

HTTP, 127.0.0.1:4444 by default. Change it with --rpc-host and --rpc-port, or turn it off with --no-rpc. Single requests and batches are both accepted.

curl -s -X POST -H 'content-type: application/json' --data '[
  {"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]},
  {"jsonrpc":"2.0","id":2,"method":"net_peerCount","params":[]},
  {"jsonrpc":"2.0","id":3,"method":"web3_clientVersion","params":[]}
]' http://127.0.0.1:4444/

[{"id":1,"jsonrpc":"2.0","result":"0x8d1804"},
 {"id":2,"jsonrpc":"2.0","result":"0x10"},
 {"id":3,"jsonrpc":"2.0","result":"Rustock/0.1.0"}]

No authentication, no rate limiting, no CORS policy. Keep it on loopback. If it must be reachable, put a reverse proxy in front that provides all three.

Block parameters

Methods taking a block accept a hex block number, or the tags latest, earliest and pending. Historical state is served from the local Unitrie, so a query at an old height is answered from disk rather than refused. A node left alone is an archive node in practice, because no collector runs unless it is enabled.

With a collector enabled, historical queries are bounded. The epoch collector keeps a window rather than all history, so eth_call, eth_getBalance and the tracing methods answer within the burial depth and error below it. That is the deliberate trade for a store that would otherwise grow without bound — but it does mean a Rustock node with collection on cannot serve the deep historical workloads an archive node can.

Chain and node info

MethodReturns
eth_blockNumberHeight of the canonical head
eth_chainIdChain id (0x1e on mainnet)
eth_syncingfalse, or {startingBlock, currentBlock, highestBlock}
eth_protocolVersionP2P protocol version
eth_gasPriceA percentile over the last 512 transactions' gas prices, floored at the block minimum × 1.1 — the price to pay for inclusion, not the validity floor
eth_mining, eth_hashrateRustock does not hash; merged mining is driven by external mining software through mnr_*
eth_accounts, eth_coinbaseEmpty — there is no wallet
eth_getBlockByHash, eth_getBlockByNumberFull block, with or without transaction objects
eth_getBlockTransactionCountByHash / ByNumberTransaction count
eth_getUncleCountByBlockHash / ByNumberUncle count
eth_getBlocksByNumberEvery block stored at a height, canonical or not, each flagged. Routine on a merged-mining chain, and the question hardest to answer from outside — canonical data is exactly what no longer mentions a sibling. Takes a decimal height as well as hex and accepts no tags, reproducing rskj's stringNumberAsBigInt parsing rather than tidying it
eth_netHashrateNetwork hashrate over a one-hour window, summing each counted block's difficulty plus its uncles'. Answers with a JSON number rather than a 0x quantity string, as rskj does
eth_pendingTransactionsThe pool's pending set
net_version, net_listening, net_peerCountChain id as a decimal string — 30, 31, 33 — as rskj returns it, despite the method's name. Plus listening flag and peer count
net_peerListConnected peers, rskj-style
web3_clientVersionRustock/0.1.0
web3_sha3Keccak-256 of the input
rpc_modulesEnabled namespaces and their versions

State queries

Served from the local Unitrie at any historical block.

MethodNotes
eth_getBalanceBalance in wei at the given block
eth_getTransactionCountNonce. With pending, reflects the pool
eth_getCodeDeployed bytecode
eth_getStorageAtA single storage cell
eth_getProofEIP-1186's envelope over unitrie node messages, not Merkle-Patricia nodes — a stock EIP-1186 verifier will not verify these

RSK account semantics are frontier-forever: an account exists if it is in the trie, and EIP-161 emptiness never applies. An account with zero balance, zero nonce and no code can still exist — and the distinction is consensus-visible. Do not infer non-existence from an all-zero reply.

Execution

MethodNotes
eth_callRuns against a forked state with full precompile and Bridge support
eth_estimateGasSame machinery, answered in one execution rather than by binary search

Calls against the Bridge precompile go through rskj's local-call gate, so the local-only getters behave as they do on rskj — including the ones that are only reachable this way. All 70 methods in rskj's dispatch table now have an arm; none of them fall through to an empty-bytes catch-all that would decode as a plausible-looking zero. What changed →

# read a contract, at a historical block
curl -s -X POST -H 'content-type: application/json' --data '{
  "jsonrpc":"2.0","id":1,"method":"eth_call","params":[
    {"to":"0x0000000000000000000000000000000001000006","data":"0x3f9b2a45"},
    "0x8d1804"]}' http://127.0.0.1:4444/

Transactions and receipts

MethodNotes
eth_sendRawTransactionValidated against the pool and live state, then broadcast to peers
eth_getTransactionByHashCanonical or pending
eth_getTransactionByBlockHashAndIndex
eth_getTransactionByBlockNumberAndIndex
eth_getTransactionReceiptStatus, gas used, logs, bloom

A quirk worth knowing before you trust a receipt. On RSK, a transaction that calls a precompile directly and makes it throw reports status = 0x1 and an understated gasUsed, while the sender is charged the entire gas limit. This is consensus-frozen rskj behaviour that Rustock reproduces deliberately, and it was reported upstream by this project. The write-up →

Logs and filters

MethodNotes
eth_getLogsAddress and topic filtering over a block range
eth_newFilterPersistent log filter
eth_newBlockFilterNew canonical heads
eth_newPendingTransactionFilterNew pool admissions
eth_getFilterChangesEverything since the last poll
eth_getFilterLogsEverything the filter has ever matched
eth_uninstallFilterRelease it

Transaction pool

MethodNotes
txpool_statusPending and queued counts
txpool_contentFull pending and queued sets, grouped by sender and nonce
txpool_inspectThe same, as human-readable summaries

Admission validates nonce, balance, intrinsic gas and chain id against live state, and pending nonces are tracked per sender so a sequence submitted at once is not rejected on the second transaction.

Broadcast is additionally rate-limited per account: a port of rskj's TxQuotaChecker, where each account accumulates virtual gas and pays a cost computed from six factors when it broadcasts or replaces a transaction. It is what stops one account flooding the shared mempool by broadcast-then-replace.

The rsk namespace

MethodNotes
rsk_protocolVersionRSK protocol version
rsk_getRawBlockHeaderByHashRaw RLP of the header — what merged mining needs
rsk_getRawBlockHeaderByNumberThe same, by height
rsk_getRawTransactionReceiptByHashRaw RLP of a receipt
rsk_getTransactionReceiptNodesByHashThe receipt trie nodes along the path to a receipt
rsk_getStorageBytesAtA storage cell as raw bytes, rather than padded to 32

Six further rsk_ methods act on the node rather than answering questions about the chain, and are gated behind --rpc-admin.

Tracing — debug_ and trace_

One inspector, rendered two ways, so the two namespaces cannot disagree about the same transaction.

MethodNotes
debug_traceTransactionOpcode stream, in the shape of rskj's DetailedProgramTrace
debug_traceBlockByHash / ByNumberThe same, for every transaction in a block
trace_transactionThe call tree — what an explorer shows as internal transactions
trace_blockEvery call tree in a block
trace_getOne trace by index — which indexes the block, as rskj does, not the transaction
trace_filterMatches whole transactions rather than individual traces, again as rskj does
debug_accountTransactionQuotaAn account's remaining virtual gas in the rate limiter
debug_wireProtocolQueueSizeDepth of the inbound wire-message queue

Do not port a Parity-shaped client straight across. rskj's traces differ in ways that silently produce wrong answers: a call to the Bridge never appears in a call tree at all, because rskj emits no subtrace for a precompile; a failed CREATE vanishes along with its whole subtree.

Tracing a historical transaction re-executes its whole block from the parent with the tracer switched on at one index, because the pre-state exists nowhere — only per-block roots are stored. The consensus block executor is shared for this rather than copied.

WebSocket subscriptions

eth_subscribe over a WebSocket on its own port, fed by one typed chain-event channel the sync service publishes to as it follows the tip.

SubscriptionNotes
newHeadsEvery canonical head as it is accepted
logsAddress and topic filtering, with reorg retraction — see below
newPendingTransactionsPool admissions
eth_unsubscribeRelease a subscription

Reorgs retract. When a branch is abandoned, its logs are re-sent with removed: true before the replacements arrive. This node sees a tip fork several times an hour, so a subscriber that never heard about one would quietly accumulate logs from blocks that are no longer on the chain. A consumer that falls behind is disconnected rather than silently skipped.

Peer scoring — sco_

A port of rskj's co.rsk.scoring: fifteen event counters per peer, kept by node id and by address, punishment whose length grows on repeat offences, and address/CIDR bans that persist across restarts. The sco_* methods inspect and override all of it — sco_peerList, sco_reputationSummary, sco_banAddress, sco_unbanAddress and the rest, matching rskj's names and shapes.

Merged mining — mnr_

The interface mining software speaks. Rustock builds block templates, selects uncles, handles the Bitcoin coinbase and RSKIP110 fork-detection data, then validates and imports submitted solutions.

MethodNotes
mnr_getWorkThe current template's work package
mnr_submitBitcoinBlockSubmit a full Bitcoin block carrying the tag
mnr_submitBitcoinBlockPartialMerkleSubmit with a partial Merkle proof
mnr_submitBitcoinBlockTransactionsSubmit with the coinbase and transaction hashes

Administrative — --rpc-admin

These act on the node rather than answering questions about the chain, so they are refused outright unless the operator passes --rpc-admin. The server binds to loopback by default, but "not reachable" is a deployment detail rather than an access-control decision — a method that deletes a database should require the operator to have asked for it.

MethodNotes
rsk_collectTrieRun an epoch garbage-collection cycle
rsk_collectTrieStatusProgress of the running cycle
rsk_pruneBlocksPrune block data below a retention floor
rsk_storageStatusStore sizes and epoch state
rsk_flushFlush pending writes to disk
rsk_shutdownStop the node cleanly

Development chain — --dev-rpc

Seven methods for driving a local chain, off unless --dev-rpc is passed. They let an RPC caller rewrite chain state, which is exactly what a test harness wants and exactly what a node on a real network must not offer.

MethodNotes
evm_mineProduce a block on demand
evm_snapshot, evm_revertTake a state snapshot and roll back to it
evm_increaseTimeMove the clock forward
evm_resetReturn the chain to genesis
evm_startMining, evm_stopMiningStart and stop automatic block production

Gas estimation in one pass

eth_estimateGas answers in one execution. The obvious implementation is a binary search — execute at the cap, halve the interval until it closes — which costs about twenty-four executions for an answer a single pass already contains.

One execution knows it if it records three things while it runs, which is what rskj has done since EthModule.internalEstimateGas:

estimate = movedRemainingGasToChild ? gasUsed + deductedRefund
                                    : maxGasUsed
if (callWithValuePerformed) estimate += STIPEND_CALL

The load-bearing idea is that RSK's gas counter is not monotonic. At a CALL it adds the callee's whole allowance and subtracts whatever comes back. The peak of that counter — not its final value — is what the caller had to hold, and therefore what the limit has to be. A contract calling out with a fixed generous stipend finishes having consumed very little while having needed a great deal, and that gap is what binary search rediscovers one halving at a time.

The two estimators do not always agree, and neither is wrong. Binary search finds the smallest limit at which the transaction does not revert; the single pass reports the limit at which it behaves as it did at the cap. They part over a CALL with a large fixed allowance, because RSK has no EIP-150 retention — calleeGas = min(remaining, requested), so a call asking for more than is left does not fail, the callee simply gets less. The transaction still succeeds at the lower limit, but the inner call then runs on different gas than it was estimated at.

The binary search is kept behind --gas-estimator-binary-search, where it also serves as the test oracle. Where the single-pass estimate is loose, and why, is written down rather than left as a comment.

eth_getProof, over a unitrie

EIP-1186 defines accountProof and storageProof[].proof as arrays of RLP-encoded Merkle-Patricia nodes. RSK has no Merkle-Patricia trie. It has a unitrie: one binary radix trie with path compression, holding accounts, code and storage together, with its own node encoding. So there is no way to satisfy the letter of the standard, and the question is what to answer instead.

Rustock answers with EIP-1186's envelope — balance, nonce, codeHash, storageHash and a storageProof entry per slot are all trie-agnostic, and are what a caller actually came for — while the proof arrays carry unitrie node messages, hex-encoded, deepest first.

A stock EIP-1186 verifier will not verify these. It fails to parse them, which is the safe failure, but it is stated here and at the top of the module rather than left to be discovered.

The encoding is not invented. rskj already exposes proofs in exactly this shape — RskModuleImpl.getTransactionReceiptNodesByHash returns the hex of each node's toMessage() in findNodes order — so this reuses RSK's established convention rather than adding a second one.

storageHash is the field needing judgement. On Ethereum it is the root of a separate storage trie; RSK has none, since storage lives beneath account_key || 0x00 in the same trie. The hash of the node at that path is the true analogue — it commits to that account's storage and nothing else — and is what this returns. No storage, no node, and the answer is zero.

Absence is an empty array, never a truncated path. A proof of absence over a radix trie needs the divergence point and its siblings, which is a different structure; emitting a partial inclusion path would look like evidence without being any.

rskj does not implement eth_getProof at all. Offering it is a deliberate divergence: the question is worth answering, and there is no standard shape RSK could satisfy instead.

Known gaps

Methods an Ethereum-shaped tool will reach for and not find. These are not yet built rather than declined — the distinction the table below does not make.

MethodWhat it would give you
eth_getBlockReceiptsEvery receipt in a block in one call, instead of N round trips
debug_traceCall, trace_callTrace a call that was never mined. The tracer and the call machinery both exist; the entry point does not
debug_storageRangeAtPaginate a contract's storage without knowing the keys
txpool_contentFromOne sender's pending and queued transactions
trace_replayTransaction and the Erigon extensionsTrace plus state diff in one response. Rustock's four trace_ methods match rskj's TraceModuleImpl exactly, which is the declared target; these five are reth-and-Erigon additions
admin_*, ots_*Standard node administration, and the Otterscan explorer API. Manual peering is the felt one: Rustock can ban a peer with sco_banAddress but cannot dial one on request

Named tracers are a structural gap, not a missing method. geth and reth accept a tracer by name on debug_trace* — callTracer, prestateTracer, 4byteTracer, and in geth's case JavaScript tracers supplied in the request. Rustock supports none of them. It produces rskj's opcode-level structLogs from debug_trace* and Parity-style call trees from trace_*, which covers what callTracer is usually wanted for — but in a different shape, so tooling that asks by name gets nothing it recognises. prestateTracer has no equivalent in any shape, and it is the one simulators need most.

Unsupported

These return an error rather than a wrong answer, and are absent by design rather than unbuilt — see known gaps for the other kind:

Namespace / methodStatusWhy
eth_sendTransaction, eth_signnot implementedNo wallet, by design — sign externally
personal_*not implementedKey management is not the node's job
Compiler methodsnot implementedRemoved from Ethereum too
db_*not implementedLegacy rskj surface
EIP-1559 and typed-transaction methodsnot applicableeth_feeHistory, eth_maxPriorityFeePerGas, eth_createAccessList, eth_blobBaseFee. RSK has no base fee, no typed transactions and no blobs — these describe a protocol RSK does not implement
Proof-of-stake methodsnot applicableThe Engine API and everything hanging off it. RSK is merged-mined with Bitcoin