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
| Method | Returns |
|---|---|
eth_blockNumber | Height of the canonical head |
eth_chainId | Chain id (0x1e on mainnet) |
eth_syncing | false, or {startingBlock, currentBlock, highestBlock} |
eth_protocolVersion | P2P protocol version |
eth_gasPrice | A 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_hashrate | Rustock does not hash; merged mining is driven by external mining software through mnr_* |
eth_accounts, eth_coinbase | Empty — there is no wallet |
eth_getBlockByHash, eth_getBlockByNumber | Full block, with or without transaction objects |
eth_getBlockTransactionCountByHash / ByNumber | Transaction count |
eth_getUncleCountByBlockHash / ByNumber | Uncle count |
eth_getBlocksByNumber | Every 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_netHashrate | Network 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_pendingTransactions | The pool's pending set |
net_version, net_listening, net_peerCount | Chain id as a decimal string — 30, 31, 33 — as rskj returns it, despite the method's name. Plus listening flag and peer count |
net_peerList | Connected peers, rskj-style |
web3_clientVersion | Rustock/0.1.0 |
web3_sha3 | Keccak-256 of the input |
rpc_modules | Enabled namespaces and their versions |
State queries
Served from the local Unitrie at any historical block.
| Method | Notes |
|---|---|
eth_getBalance | Balance in wei at the given block |
eth_getTransactionCount | Nonce. With pending, reflects the pool |
eth_getCode | Deployed bytecode |
eth_getStorageAt | A single storage cell |
eth_getProof | EIP-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
| Method | Notes |
|---|---|
eth_call | Runs against a forked state with full precompile and Bridge support |
eth_estimateGas | Same 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
| Method | Notes |
|---|---|
eth_sendRawTransaction | Validated against the pool and live state, then broadcast to peers |
eth_getTransactionByHash | Canonical or pending |
eth_getTransactionByBlockHashAndIndex | |
eth_getTransactionByBlockNumberAndIndex | |
eth_getTransactionReceipt | Status, 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
| Method | Notes |
|---|---|
eth_getLogs | Address and topic filtering over a block range |
eth_newFilter | Persistent log filter |
eth_newBlockFilter | New canonical heads |
eth_newPendingTransactionFilter | New pool admissions |
eth_getFilterChanges | Everything since the last poll |
eth_getFilterLogs | Everything the filter has ever matched |
eth_uninstallFilter | Release it |
Transaction pool
| Method | Notes |
|---|---|
txpool_status | Pending and queued counts |
txpool_content | Full pending and queued sets, grouped by sender and nonce |
txpool_inspect | The 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
| Method | Notes |
|---|---|
rsk_protocolVersion | RSK protocol version |
rsk_getRawBlockHeaderByHash | Raw RLP of the header — what merged mining needs |
rsk_getRawBlockHeaderByNumber | The same, by height |
rsk_getRawTransactionReceiptByHash | Raw RLP of a receipt |
rsk_getTransactionReceiptNodesByHash | The receipt trie nodes along the path to a receipt |
rsk_getStorageBytesAt | A 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.
| Method | Notes |
|---|---|
debug_traceTransaction | Opcode stream, in the shape of rskj's DetailedProgramTrace |
debug_traceBlockByHash / ByNumber | The same, for every transaction in a block |
trace_transaction | The call tree — what an explorer shows as internal transactions |
trace_block | Every call tree in a block |
trace_get | One trace by index — which indexes the block, as rskj does, not the transaction |
trace_filter | Matches whole transactions rather than individual traces, again as rskj does |
debug_accountTransactionQuota | An account's remaining virtual gas in the rate limiter |
debug_wireProtocolQueueSize | Depth 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.
| Subscription | Notes |
|---|---|
newHeads | Every canonical head as it is accepted |
logs | Address and topic filtering, with reorg retraction — see below |
newPendingTransactions | Pool admissions |
eth_unsubscribe | Release 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.
| Method | Notes |
|---|---|
mnr_getWork | The current template's work package |
mnr_submitBitcoinBlock | Submit a full Bitcoin block carrying the tag |
mnr_submitBitcoinBlockPartialMerkle | Submit with a partial Merkle proof |
mnr_submitBitcoinBlockTransactions | Submit 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.
| Method | Notes |
|---|---|
rsk_collectTrie | Run an epoch garbage-collection cycle |
rsk_collectTrieStatus | Progress of the running cycle |
rsk_pruneBlocks | Prune block data below a retention floor |
rsk_storageStatus | Store sizes and epoch state |
rsk_flush | Flush pending writes to disk |
rsk_shutdown | Stop 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.
| Method | Notes |
|---|---|
evm_mine | Produce a block on demand |
evm_snapshot, evm_revert | Take a state snapshot and roll back to it |
evm_increaseTime | Move the clock forward |
evm_reset | Return the chain to genesis |
evm_startMining, evm_stopMining | Start 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.
| Method | What it would give you |
|---|---|
eth_getBlockReceipts | Every receipt in a block in one call, instead of N round trips |
debug_traceCall, trace_call | Trace a call that was never mined. The tracer and the call machinery both exist; the entry point does not |
debug_storageRangeAt | Paginate a contract's storage without knowing the keys |
txpool_contentFrom | One sender's pending and queued transactions |
trace_replayTransaction and the Erigon extensions | Trace 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 / method | Status | Why |
|---|---|---|
eth_sendTransaction, eth_sign | not implemented | No wallet, by design — sign externally |
personal_* | not implemented | Key management is not the node's job |
| Compiler methods | not implemented | Removed from Ethereum too |
db_* | not implemented | Legacy rskj surface |
| EIP-1559 and typed-transaction methods | not applicable | eth_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 methods | not applicable | The Engine API and everything hanging off it. RSK is merged-mined with Bitcoin |