The Stack
The Rust stack BLCK is building for Robinhood Chain, and why each piece is there.
The shape of the problem
Strict FCFS at a single sequencer. Blocks about every 100 ms when there is traffic (measured 97.7 ms mean over 621 blocks in a 60 s mainnet sample). No public mempool. A public sequencer feed that carries what the sequencer has ordered, independent of block RPC; whether it runs ahead, and by how much, is unmeasured. A submit endpoint that rejects reads (eth_chainId returns -32601) and exists for eth_sendRawTransaction.
So the loop is: read the feed, decode the message, simulate, craft, sign, send on a warm connection. Every step is on the hot path. That is why the workspace is Rust: no garbage collector pause, no interpreter, one binary per module, and the same crates from the probe up to the executor. Foundry handles the Solidity side.
Built for the sequencer edge, not for FCFS
BLCK calls the discipline sequencer-edge engineering rather than FCFS engineering, and the stack is chosen so that it survives a rule change. Timeboost is live on Arbitrum One and the ArbitrumDAO has voted to move Arbitrum One to Priority Gas Auctions. Nothing equivalent is announced for Robinhood Chain (open question Q3). If a priority auction lands here, this is what changes in the stack:
- The feed decoder, the probe, and the warm-connection send path stay as they are. Arrival time still orders everything outside the paid lane.
- blck-sim gains a bid input in the profit check.
- blck-probe output has to separate paid-lane inclusion from plain inclusion.
- Nothing in the crate table below is replaced. An auction client is added next to the existing sender.
Today there is no bid. The only ordering input the stack controls is arrival time.
The crates
Versions are the ones pinned in the workspace Cargo.toml.
| Piece | Version | Job in BLCK |
|---|---|---|
| alloy | 1, full features | Transaction crafting, signing, typed transaction envelopes, ABI bindings, HTTP and WebSocket JSON-RPC providers, eth_sendRawTransaction to the submit endpoint. |
| alloy-primitives | 1 | Address, the address! macro, and fixed-width integers. blck-common depends on alloy-primitives, serde, and anyhow only, so it stays light. |
| revm | not yet pinned | Local EVM. blck-sim runs candidate transactions against state forked from a mainnet RPC before anything is signed. Added at Milestone 3. |
| tokio | 1, full features | The event loop. One task per stream: feed reader, RPC poller, sender. |
| tokio-tungstenite | 0.26, rustls-tls-webpki-roots | The sequencer feed and any third-party WebSocket RPC. |
| reqwest | 0.12, json and rustls-tls only | HTTP JSON-RPC, and the Robinhood stock-token REST API in blck-gaps. |
| rustls | 0.23, ring | TLS provider. alloy and the HTTP and WebSocket clients pull different rustls backends, so blck-probe installs ring as the default provider at startup. |
| base64, hex | 0.22, 0.4 | base64 decodes the feed's l2Msg. hex handles key material in keygen and send mode. |
| clap | 4, derive and env | CLI for every module. Flags fall back to environment variables. |
| dotenvy | 0.15 | Loads .env. |
| serde, serde_json | 1 | Feed frames, probe output, config. |
| tracing, tracing-subscriber | 0.1, 0.3 | Structured logs with RUST_LOG style filters. |
| futures-util, url, chrono, anyhow, thiserror | as pinned | Stream combinators, URL parsing, timestamps, error handling. |
Release profile: lto = "fat", codegen-units = 1, panic = "abort". Minimum rust-version 1.85. Toolchain channel is stable with clippy and rustfmt; the dev machine runs 1.98.1.
Two practical notes that cost time if you do not know them. The official RPC returns HTTP 403 to the default Python urllib user agent, so send a normal user agent from any client. The curl build on the dev machine has no WebSocket support, so the feed client is the Rust one, not a shell one-liner.
Foundry
Foundry 1.8.3 (forge, cast, anvil). contracts/ is the Foundry project for AtomicArbExecutor and its tests (Milestone 3). cast call is verified working from this machine. forge test --fork-url gives the Solidity tests real mainnet state to run against, reading the live pools without sending anything. The Rust side gets the same thing from blck-sim, where revm forks through RPC without a separate process.
The feed decoder
The feed speaks JSON over WebSocket. A frame looks like this:
{"version":1,"messages":[{"sequenceNumber":N,"message":{"message":{"header":{"kind":3,...},"l2Msg":"<base64>"}}}]}
blockMetadata is null on all 789 captured messages. That is one of four checks (with the receipt field, the RPC namespaces, and a string search of the capture) behind the statement that Timeboost is not enabled.
Decoding. Steps 1 and 2 are observed. Steps 3 to 5 follow Nitro's arbos/parse.go and are not yet validated by BLCK against a live capture.
- Track
sequenceNumber. A gap means you missed messages. Log it, do not paper over it. - Base64-decode
l2Msginto bytes. - The first byte is the L2 message kind. This is a different field from
header.kindin the frame; the two are separate enums that happen to share the value 3. - Kind 3 is a batch: a sequence of sub-messages, each with an 8 byte big-endian length prefix and its own kind byte. Recurse. The decoder in
toolkit/blck-probe/src/feed.rshardcodes the 8 byte prefix. - Kind 4 is a signed transaction. The remaining bytes are an EIP-2718 typed transaction envelope, or a legacy RLP transaction. alloy's
TxEnvelopedecodes either. - Other kinds exist. Log the kind and skip until a module needs it.
Validated against captured frames: blck-probe's decoder is tested against a real feed message, and blck-sniper decodes live launch transactions out of the same framing.
On connect from this machine, 5 messages arrived within about 2 s. The feed hosts CNAME to customer-origin.offchainlabs.com behind Cloudflare, so there is at least one proxy hop in front of the feed origin. Whether that origin is the sequencer host is not known. blck-probe measures feed-versus-RPC delay rather than assuming it. Chainstack publishes a feed decoder for this chain (github.com/chainstacklabs/robinhood-chain-sequencer-feed); compare against it.
Endpoints
Verified live 2026-09-16. The first-party endpoints (official RPC, submit endpoint, feed, explorer), the PublicNode WSS rows, and the L1 contract addresses are constants in toolkit/blck-common/src/chains.rs and addresses.rs. The third-party HTTP RPC list and the stock-token REST URLs are not in the crate yet.
| Kind | Mainnet 4663 |
|---|---|
| Official HTTP RPC (rate limited) | https://rpc.mainnet.chain.robinhood.com |
| Sequencer submit endpoint | https://sequencer.mainnet.chain.robinhood.com |
| Sequencer feed | wss://feed.mainnet.chain.robinhood.com |
| Public WSS RPC (third party) | wss://robinhood-rpc.publicnode.com |
| Other public HTTP RPC | robinhood-rpc.publicnode.com, robinhood.drpc.org, robinhood.rpc.blxrbdn.com, robinhood.api.pocket.network, rpc.ordofi.network (rpc.arrowrpc.com was down) |
| Explorer | https://robinhoodchain.blockscout.com |
| L1 Rollup | 0x23A19d23e89166adedbDcB432518AB01e4272D94 (Ethereum) |
| L1 SequencerInbox | 0xBd0D173EEb87D57A09521c24388a12789F33ba96 |
| L1 Delayed Inbox | 0x1A07cc4BD17E0118BdB54D70990D2158AbAD7a2D |
| Stock token REST | https://api.robinhood.com/rhj/assets and /rhj/prices/{SYMBOL} (bid, ask, isTradingHalt, dailyTradingVolume) |
Robinhood publishes no first-party WebSocket RPC. The public WSS rows are PublicNode. Keyed endpoints exist from Alchemy (the docs-recommended provider), QuickNode, Chainstack, dRPC, Blockdaemon, Validation Cloud, and GetBlock. The submit endpoint rejects reads; the official RPC is for reads.
The L1 rows matter for one thing on the searcher side: force inclusion through the Delayed Inbox is 4 days (delaySeconds 345600 from SequencerInbox.maxTimeVariation() on both networks), not the Arbitrum default of about a day. It is meant as a censorship backstop, not a submission path. One caveat: L2BEAT reports an ArbOS 61 transaction-filtering precompile at 0x74 on this chain that can defeat force inclusion. BLCK has not probed it (open question Q4). Do not treat force inclusion as a guaranteed backstop until that is checked.
Environment variables
Everything secret or endpoint-shaped comes from the environment. Copy .env.example to .env and fill it in. .env, .env.*, *.key, and *.pem are gitignored; only .env.example is tracked.
| Variable | Meaning |
|---|---|
BLCK_MAINNET_RPC_URL, BLCK_MAINNET_WS_URL, BLCK_MAINNET_FEED_URL | Mainnet 4663 endpoints. Defaults match the table above. |
BLCK_PRIVATE_KEY | Signing key for modules that send (blck-probe send mode, blck-arb live mode). Hex, 0x-prefixed. |
BLCK_PROBE_EXTRA_RPCS | Optional. Comma-separated label=url pairs for endpoint comparisons in blck-probe. |
BLCK_PROBE_REGION | Optional. Free-text region label written into probe output. Runs without it are marked unspecified, and only labeled runs get committed. |
BLCK_PRIVATE_KEY is a dedicated key that holds only what you are willing to spend on a run. blck-probe keygen generates one and prints the address. It prints the key only with --show-key and never writes files; you paste it into the gitignored .env yourself, or use scripts/set-key.sh, which prompts with hidden input. Because BLCK targets mainnet, that key spends real ETH: keep the balance small and rotate it when it has been exposed.
Module to stack map
| Module | Milestone | Stack pieces | Status |
|---|---|---|---|
| blck-common | 0 | alloy-primitives, serde, anyhow. Verified chain constants, endpoints, addresses. | Building |
| blck-probe | 2 | tokio, reqwest (RPC round trip), tokio-tungstenite (feed-versus-RPC delay, block cadence via newHeads), alloy (send mode for inclusion delay), base64 for the feed payload, hex for key material, clap, chrono. Will write JSON matching the probe schema. The schema lives at benchmarks/schema/probe-result.schema.json and scripts/check-probe-output.sh checks every committed file against it. | Live: all modes have run, nine committed runs |
| blck-sim | 3 | revm against forked state fetched over RPC, alloy for ABI and transaction types, Foundry tests alongside in contracts/. Gas model has to handle ArbGasInfo.getL1BaseFeeEstimate returning 0 on mainnet (open question Q6). | Planned |
| blck-arb | 4 | alloy providers and signer, blck-sim for the profit check, AtomicArbExecutor from contracts/, tokio for the quote loop. Paper mode first. | Planned |
| blck-sniper | 5 | Feed decoder from blck-probe, alloy event decoding for Pons factories and CurveBuy and CurveSell, blck-sim, submit endpoint on a warm connection. | Planned |
| blck-gaps | 5 | reqwest for the Robinhood price API, alloy calls to Chainlink aggregators and to Uniswap v3 pools, StateView, and V4Quoter, chrono for session state. Read-only. | Planned |
blck-sim, blck-arb, blck-sniper, and blck-gaps all run today, and each carries a README stating what it can do and what it cannot. blck-gaps is read-only and complete for its milestone; the other three are BUILDING, with the missing piece named in each README.
Mainnet only
BLCK targets mainnet, chain 4663. There is no rehearsal chain in the picture, so the safety model is different: every tool that sends refuses unless you pass an explicit command line flag and set an environment variable, and anything untested first runs against a local fork of mainnet state.
A fork gives you the real thing: Uniswap v2, v3, and v4, USDG, the stock tokens, Morpho, and the Pons factories, all at their mainnet addresses and with real liquidity. blck-sim forks through RPC for Rust code; anvil --fork-url and forge test --fork-url do the same for Solidity.
What a fork does not give you: the sequencer. Ordering, feed timing, and inclusion delay can only be measured against the live chain, which is what blck-probe does and why its send mode exists.
So the rehearsal plan is:
- blck-probe: live. rpc, feed, blocks, and send have all run on mainnet; six runs are committed under benchmarks/data.
- blck-sim and AtomicArbExecutor: fork tests against mainnet state, no deployment.
- blck-arb: paper mode against mainnet state, then live only behind the double guard.
- blck-sniper: read-only watching and dry-run calldata, no sending.
- blck-gaps: read-only, and it never sends.