Warning
Rustock is a prototype implemented in an AI-assisted manner. It should never be used in production. The code has not been reviewed in depth and has not been security audited. It may — and will — break and contain bugs.
Use at your own risk.
A Rootstock (RSK) full node implementation in Rust. Rustock syncs and validates blocks from the RSK network using Bitcoin merged mining proofs, executes every transaction against an RSK-compatible Unitrie world state, follows the chain tip in real time, and exposes an rskj-compatible JSON-RPC interface with full state, call, and log support.
Coming from rskj? docs/rskj-to-rustock-map.md maps the two codebases package by package — the rskj class you already know on one side, the rustock file that answers to it on the other — and lists what each side has that the other does not.
- Full block sync: Skeleton-based bulk header sync with parallel chunk downloads across multiple peers, followed by block body downloads and real-time tip following via
NewBlockHashes. - EVM execution: REVM-based block execution pipeline that reproduces RSK semantics — transaction validation, gas accounting, state transitions, receipt generation, and per-block REMASC reward distribution.
- Unitrie world state: A from-scratch implementation of RSK's Unitrie (radix-2 binary trie with shared paths and embedded values), including
eth_getProof-friendly node access, key mapping for accounts/code/storage, and RocksDB-backed persistence with a write cache. - All RSK precompiles: secp256k1 recovery, block header introspection, HD wallet utilities, the BTC–RSK Bridge (two-way peg, federation management, BTC SPV header chain, governance), and REMASC.
- Consensus validation: Full verification of Bitcoin merged mining (AuxPow) proofs, difficulty adjustment, gas limits, timestamps, and all RSK consensus rules including activation-height-gated RSKIPs.
- Chain reorganization: Detects competing forks, compares total difficulty, and rewrites canonical chain pointers when a heavier fork is found.
- Transaction pool: Validates pending transactions against the live state (nonce, balance, intrinsic gas, chain ID), tracks pending nonces per sender, and exposes status to the RPC layer.
- P2P networking: Full RLPx encryption (inbound and outbound), Kademlia-based UDP discovery, peer exchange, and persistent node tables.
- Transaction relay: Receives transaction messages from peers, validates and admits them to the pool, and rebroadcasts to all other connected peers.
- Serving peers: Responds to
BlockHeadersRequest,BlockHashRequest,SkeletonRequestandBodyRequest, and — for peers that negotiated them — the six snapshot messages and thersk/63extensions. - JSON-RPC server: rskj-compatible HTTP API with
eth,net,web3,rpc, andrskmodules — including state queries,eth_call/eth_estimateGas, transaction and receipt lookups, log filtering, and persistent filters. - Snapshot sync: Joins the chain by downloading a recent state trie instead of executing from genesis — as a client and as a server. The header chain to the checkpoint is verified first under the same proof-of-work rules as a full sync, and every chunk of state is checked against that chain's state root. Interoperates with rskj in both directions (docs/cross-client-snap-sync.md).
- Header freezer: Settled headers, and the uncle headers they reference, move out of RocksDB into append-only files with a fixed-width index, so a run of consecutive blocks is a run of adjacent bytes (docs/freezer.md).
- Block pruning: Deletes history below a configurable depth, automatically or on request, clamped to a floor the block-info precompiles and reorg re-execution require (docs/block-pruning.md).
- Protocol extensions (
rsk/63): Announces the range of blocks this node can serve, and can carry the uncle headers a trunk header references — which is what lets a node compute cumulative work from a header walk, since in RSK uncle difficulty counts toward total difficulty. - Storage: RocksDB-backed persistence for headers, bodies, receipts, total difficulty, canonical chain mappings, Bridge events, and Unitrie nodes.
-
Rust (edition 2021)
-
RocksDB system libraries (usually handled automatically by
rust-rocksdb) -
A C/C++ toolchain plus
libclang—librocksdb-syscompiles vendored C++, andzstd-sysgenerates its bindings withbindgen, which needslibclang.soand clang's builtin headers at build time:# Debian/Ubuntu sudo apt install build-essential clang libclang-dev # Fedora/RHEL sudo dnf install gcc gcc-c++ clang-devel # macOS (ships with the Xcode command line tools) xcode-select --install
If
bindgenstill reportsUnable to find libclang, point it at the library explicitly, e.g.export LIBCLANG_PATH=/usr/lib/llvm-21/lib.
cargo build --workspace --releaseStart the full node on RSK Mainnet (default):
cargo run -p rustock-cli --release -- --port 30303 --data-dir ./data --log-to-stdoutLogs are written to <data-dir>/rustock.log by default (with daily rotation). Use --log-to-stdout for console output.
| Flag | Default | Description |
|---|---|---|
--port |
30303 |
P2P listen port |
--data-dir |
./data |
Data directory for RocksDB and logs |
--network-id |
30 |
Network ID (30 = mainnet, 31 = testnet, anything else = regtest) |
--secret-key |
auto-generated | Hex-encoded secp256k1 private key |
--log-level |
info |
trace, debug, info, warn, error, or RUST_LOG-style directives |
--log-to-stdout |
false |
Log to console instead of file |
--rpc-port |
4444 |
JSON-RPC HTTP port |
--rpc-host |
127.0.0.1 |
JSON-RPC bind address |
--no-rpc |
false |
Disable the JSON-RPC server |
--external-ip |
none | External IP to advertise in discovery (e.g. 203.0.113.42) |
Commonly used beyond the basics:
| Flag | Default | Description |
|---|---|---|
--snap-sync |
false |
Join by downloading a recent state instead of executing from genesis |
--snap-server |
false |
Serve snapshots of this node's state to peers that ask |
--prune-blocks |
false |
Delete block history below --prune-keep-depth as the node runs |
--prune-keep-depth |
100000 |
Blocks of history to keep; clamped up to 8,000 |
--freezer |
true |
false keeps settled headers in RocksDB instead of the freezer |
--read-only |
false |
Open both databases read-only and write nothing |
--follow-up |
true |
false serves what is held and does not sync, execute or follow |
--mine |
false |
Build blocks and serve the mnr_* merged-mining namespace |
--rpc-admin |
false |
Enable the administrative RPC methods |
--bootnodes |
network default | Peers to dial; with --closed-network, the only ones |
That is a selection. The node has ~96 flags, every one of them documented with
its default in node.example.toml, which is the reference
rather than this table. node-light.example.toml is
a smaller starting point, and alerts.example.toml
configures peg-out and node-health alerting.
cargo test --workspace786 tests covering consensus validation, RLP encoding, P2P handshakes, sync state machines, storage, the Unitrie, EVM execution, every RSK precompile (including the full Bridge surface and REMASC), the transaction pool, RPC methods, transaction relay, and chain reorganizations.
A synced rskj database can be imported instead of syncing from peers — about an hour against days. The two schemas differ structurally (rskj uses one RocksDB per datasource plus a MapDB index; rustock uses one RocksDB with column families), so this converts rather than copies:
rustock --import-rskj /path/to/database/mainnet --data-dir ./data
rustock --metadata-in-memory --data-dir ./dataSee docs/rskj-import.md for the schema comparison, all
options, and measured performance. tools/dump-index is a
companion Java tool for reading rskj's MapDB index directly, used as an
independent cross-check.
Supply chain checks run in CI on every push and pull request, and on a daily schedule so that advisories published against already-pinned dependencies are caught even when nobody touches the code. To run the same checks locally:
cargo install cargo-deny --locked
cargo deny check # advisories, bans, licenses, sourcesThe policy lives in deny.toml. What it enforces:
- crates.io only. Unknown registries and git dependencies are rejected.
Published crates.io versions are immutable, so together with the checksums in
Cargo.locka compromised maintainer cannot alter a version already depended on — only publish a new one, which the lockfile will not pick up until someone deliberately runscargo update. - No yanked crates. A yank usually signals a withdrawn or compromised release.
- No wildcard version requirements, which would defeat pinning.
- Known advisories, with any exception recorded in
deny.tomlalongside the reasoning for it.
Cargo.lock is committed and must stay that way: it is the primary supply chain
control, and CI verifies it is current (cargo metadata --locked) and that every
non-workspace dependency carries a checksum. Treat lockfile diffs as
security-relevant during review — a malicious dependency arrives through
Cargo.lock, not through the Rust source.
Prefer [workspace.dependencies] when adding a dependency used by more than one
crate, so two crates cannot silently pin different versions of it.
The RPC server is compatible with rskj's JSON-RPC 2.0 interface. Supported methods:
Chain and node info:
eth_blockNumber,eth_chainId,eth_syncing,eth_protocolVersioneth_gasPrice(a percentile over recent transactions, not the block minimum — see docs/gas-price.md),eth_mining,eth_hashrate,eth_accounts,eth_coinbaseeth_getBlockByHash,eth_getBlockByNumbereth_getBlockTransactionCountByHash,eth_getBlockTransactionCountByNumbereth_getUncleCountByBlockHash,eth_getUncleCountByBlockNumbereth_getUncleByBlockHashAndIndex,eth_getUncleByBlockNumberAndIndexnet_version,net_peerCount,net_listening,net_peerListweb3_clientVersion,web3_sha3rpc_modulesrsk_protocolVersion,rsk_getRawBlockHeaderByHash,rsk_getRawBlockHeaderByNumber
State queries (served from the local Unitrie at any historical block):
eth_getBalance,eth_getTransactionCount,eth_getCode,eth_getStorageAt
Execution:
eth_call,eth_estimateGas(run against a forked state with full precompile and Bridge support)
Transactions and receipts:
eth_sendRawTransaction(validates against the pool and broadcasts to peers)eth_getTransactionByHash,eth_getTransactionByBlockHashAndIndex,eth_getTransactionByBlockNumberAndIndexeth_getTransactionReceipt
Logs and filters:
eth_getLogseth_newFilter,eth_newBlockFilter,eth_newPendingTransactionFiltereth_getFilterChanges,eth_getFilterLogs,eth_uninstallFilter
Pool:
-
txpool_status,txpool_content,txpool_inspectThese follow rskj's
TxPoolModuleImpl, not go-ethereum's txpool namespace: sender keys are bare hex with no0x, each nonce maps to an array of transactions, andtxpool_statuscounts are JSON numbers rather than hex strings. See docs/rskj-vs-geth.md.
Debug:
-
debug_wireProtocolQueueSize,debug_accountTransactionQuota -
debug_traceTransaction,debug_traceBlockByHash,debug_traceBlockByNumber(within the state-retention window)rskj's
debug_*, not go-ethereum's — the two namespaces barely overlap. See docs/debug-namespace.md.
Trace:
-
trace_transaction,trace_block,trace_get,trace_filter(within the state-retention window)Call trees, in rskj's shape rather than OpenEthereum's — the differences are not cosmetic, and precompile calls (the Bridge included) never appear. See docs/trace-namespace.md.
Peer scoring:
-
sco_banAddress,sco_unbanAddress,sco_bannedAddresses,sco_peerList,sco_clearPeerScoring,sco_reputationSummary, plussco_isWelcomeA port of rskj's
co.rsk.scoring. Bans persist across restarts, which rskj's do not. See docs/peer-scoring.md.
WebSocket + subscriptions:
eth_subscribe/eth_unsubscribeover a WebSocket on its own port (--ws, default port 4445; off by default, as rskj's is).newHeads,logs(withremovedon a reorg) andnewPendingTransactions. Every HTTP method works over the socket too. See docs/websocket-subscriptions.md.
Unsupported (returns error): mining (eth_sendTransaction, eth_sign, compilers), and the personal_*, evm_*, db_* namespaces.
crates/
cli/ Main entry point, CLI argument parsing, genesis bootstrap
core/ Base types (Header, Block, Transaction, Receipt), consensus rules, chain config
trie/ RSK Unitrie: nodes, paths, account encoding, key mapping
execution/ EVM executor, block processor, all RSK precompiles, BTC–RSK Bridge, REMASC
storage/ RocksDB persistence for headers, bodies, receipts, and trie nodes (with write cache)
networking/ P2P protocol (RLPx, discovery, sessions, peer management)
sync/ Sync state machine, header + body pipeline, transaction pool, transaction relay
rpc/ JSON-RPC HTTP server (axum-based) with state, call, log, and filter support
alerts/ Peg-out and node-health alerting; an observer with no hook in execution or sync
Log timestamps are UTC by default and RFC 3339 with one subsecond digit, so the offset travels with every line rather than being something a reader has to know.
--log-timezone utc # default: 2026-09-25T21:13:45.9+00:00
--log-timezone -03:00 # 2026-09-25T18:13:45.9-03:00
--log-timezone local # read from the machine, once, at start-up
or timezone under [log] in the config file. The offset is also stated once
at start-up (Log timestamps are GMT-03:00), so a log opened later says what
it is in words as well as in every timestamp.
local is resolved once, before the runtime starts: the time crate will
not determine a local offset in a multithreaded process, because another
thread changing TZ concurrently is a data race. A consequence is that
local does not follow a daylight-saving transition mid-run. A fixed offset
avoids the question, and is exactly right for zones without DST.
Note this is rustock's own timestamp. Under systemd, journalctl prints its
own timestamp first, in the viewer's timezone — TZ=America/Argentina/Buenos_Aires journalctl -u rustock
shifts that one, with no node configuration at all.
Rustock executes blocks and maintains full state, but it is not yet feature-complete relative to rskj. Notable gaps:
- Mining is single-node only. Rustock builds blocks, serves the
mnr_*merged-mining namespace and imports solutions (--mine, see docs/merged-mining.md), but it has no outbound block announcement, so a mined block reaches peers only when they ask for it. - No archive mode. The trie store keeps every node it writes, so historical state is queryable as long as the underlying nodes have not been collected, but there is no explicit archive-vs-pruning policy.
- Bridge methods are complete but unevenly exercised. All 70 methods in the Bridge table are dispatched, transaction-callable and local-only alike. The transaction-callable ones are proven by whole-chain replay against mainnet; the local-only getters are proven only by unit tests, because mainnet history does not call them.
- Tracing needs recent state.
debug_trace*andtrace_*re-execute a transaction's block from its parent's state, so they answer only within the GC burial depth (4,000 blocks by default) and return an error past it.trace_filterrecomputes rather than reading an index, as rskj does. - Peer scoring credits blocks on execution, not on arrival. A body is
checked against its header the moment it arrives — the only moment the
sender is unambiguous — and a mismatch is charged as
INVALID_BLOCKand re-fetched from somebody else.VALID_BLOCKwaits until the block executes to the state root its header claims, which is stricter than rskj, whose credit comes from validation. See docs/peer-scoring.md. - No wallet / account management.
eth_sendTransaction,eth_sign, and thepersonal_*namespace are intentionally not supported — sign transactions externally and submit them viaeth_sendRawTransaction.
MIT