How the Rust Fast-Broadcast Module Integrates with the Node.js Gateway for HFT Arbitrage Race Detection
The Rust fast-broadcast module integrates with the Node.js gateway via a stdin/stdout IPC bridge, allowing CloddsBot to race signed transactions across multiple RPC endpoints in sub-millisecond latency without blocking the JavaScript event loop.
In high-frequency trading (HFT) arbitrage, winning the race to submit a transaction to the fastest accepting node can mean the difference between profit and loss. The CloddsBot repository implements a cross-language pipeline where the Node.js gateway orchestrates strategy logic while a compiled Rust binary handles the network-intensive race condition. This architecture eliminates garbage collection pauses and event-loop contention from the hot path of transaction submission.
Why Rust for High-Frequency Trading Race Detection
The race-to-accept pattern across multiple RPC endpoints represents a hot-path where microseconds matter. JavaScript’s garbage collector and single-threaded event loop introduce unpredictable latency spikes that can disqualify an otherwise profitable arbitrage opportunity.
According to the source code in rust/fast-broadcast/src/lib.rs, the core logic resides in the broadcast_race function (lines 104‑166). This implementation leverages Rust’s async runtime without a garbage collector, enabling thousands of concurrent HTTP POSTs to eth_sendRawTransaction with minimal overhead. The function performs three critical operations:
- Deduplicates the provided RPC URLs to avoid redundant work.
- Spawns a
tokio::task::JoinSetof async workers, each issuing a raw-transaction POST viasend_raw_transaction. - Returns the first successful response as a
BroadcastRaceResultor aggregates errors if every endpoint fails.
The Node.js IPC Wrapper Architecture
The TypeScript side of the integration acts as a thin IPC façade defined in src/evm/fast-broadcast.ts. Rather than implementing race logic in JavaScript, the wrapper spawns the Rust binary as a child process and communicates via JSON lines over stdin/stdout.
Binary Resolution and Process Spawning
The wrapper first resolves the path to the compiled binary using the FAST_BROADCAST_BIN environment variable or falls back to the default release path. The resolveBinaryPath() function (lines 35‑41) constructs the path <repo>/rust/fast-broadcast/target/release/fast-broadcast, then spawns the process with piped stdio:
const child = spawn(binaryPath, [], {
stdio: ['pipe', 'pipe', 'pipe']
});
This invocation occurs at line 57 in src/evm/fast-broadcast.ts.
Request-Response Protocol
Once spawned, the Node.js gateway writes a JSON-encoded request to the child’s stdin containing the raw transaction, RPC URLs, and timeout:
const request = JSON.stringify({ rawTx, rpcUrls, timeoutMs });
child.stdin.write(request + '\n');
The Rust binary executes broadcast_race and writes the result as a single JSON line to stdout. The TypeScript wrapper parses this response (lines 70‑82) and resolves the promise with a BroadcastRaceResult object containing the transaction hash, winning RPC URL, latency metrics, and endpoint count. Error handling surfaces both runtime failures child.on('error') and worker-reported errors through a uniform Error interface.
End-to-End Arbitrage Execution Flow
The integration follows a four-step pipeline when executing HFT arbitrage strategies:
- Gather RPC endpoints –
getRaceUrls(chain)pulls the primary RPC and configured fallbacks from the chain configuration located insrc/evm/multichain.ts(lines 57‑60). - Prepare the transaction – An arbitrage skill (such as the Odos swap integration) constructs a transaction object (
swapTx). - Invoke the race – When multiple RPCs are available, the skill calls
signAndBroadcastRace(seesrc/evm/odos.ts, lines 306‑313). The Rust worker races the signed transaction across all endpoints, returning the hash from the fastest accepting node. - Continue processing – The skill logs the winning URL and latency, then awaits confirmation using the standard provider’s
waitForTransactionmethod.
Because the Rust worker runs as an isolated subprocess, the Node.js event loop remains available to process concurrent skills, making the architecture suitable for high-frequency scenarios where every millisecond counts.
Key Integration Points and Source Files
| Component | Purpose | Source Location |
|---|---|---|
| Rust core | Parallel submission of signed raw transactions to many RPCs, returning the first success. | rust/fast-broadcast/src/lib.rs |
| Rust binary entry point | Reads JSON from stdin, calls broadcast_race, writes JSON to stdout. |
rust/fast-broadcast/src/main.rs |
| Node.js wrapper | Spawns the binary, marshals request/response JSON, exports broadcastRace and signAndBroadcastRace. |
src/evm/fast-broadcast.ts |
| RPC list provider | Returns primary and fallback RPC URLs for a given chain with deduplication. | src/evm/multichain.ts (lines 57‑60) |
| Skill usage example | Odos swap arbitrage leverages signAndBroadcastRace when fallbacks are present. |
src/evm/odos.ts (lines 306‑313) |
Implementation Examples
Direct Rust Worker Invocation
For scenarios where the transaction is already signed, use the broadcastRace function to invoke the Rust binary directly:
import { broadcastRace } from './evm/fast-broadcast';
// rawTx is a 0x-prefixed signed transaction string
const rawTx = '0x...';
const rpcUrls = [
'https://eth-mainnet.alchemyapi.io/v2/…',
'https://rpc.ankr.com/eth',
];
const timeoutMs = 5_000;
const result = await broadcastRace(rawTx, rpcUrls, timeoutMs);
console.log('Tx accepted by', result.wonBy, 'in', result.latencyMs, 'ms');
Sign-and-Race Workflow
When the transaction requires signing, the signAndBroadcastRace convenience function handles both operations:
import { signAndBroadcastRace } from './evm/fast-broadcast';
import { Wallet, type TransactionRequest } from 'ethers';
import { getRaceUrls } from './evm/multichain';
async function executeArbitrage(wallet: Wallet, tx: TransactionRequest) {
const raceUrls = getRaceUrls('ethereum');
const { hash, wonBy, latencyMs } = await signAndBroadcastRace(
wallet,
tx,
raceUrls,
4_000
);
console.log(`Arbitrage tx ${hash} won via ${wonBy} (took ${latencyMs} ms)`);
return hash;
}
This pattern appears in the Odos swap integration at lines 306‑313 of src/evm/odos.ts.
Rust Entry Point Structure
The Rust binary itself operates as a minimal JSON bridge. The main.rs file creates a Tokio runtime, deserializes the request from stdin, executes broadcast_race, and serializes the result to stdout:
fn main() {
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
let req: Request = serde_json::from_str(&input).unwrap();
let result = tokio::runtime::Runtime::new()
.unwrap()
.block_on(broadcast_race(
&req.raw_tx,
&req.rpc_urls,
Duration::from_millis(req.timeout_ms)
));
println!("{}", serde_json::to_string(&result).unwrap());
}
This implementation resides in rust/fast-broadcast/src/main.rs.
Summary
- The Rust fast-broadcast module eliminates GC latency from the transaction submission hot-path by handling the RPC race in a compiled binary using
tokio::task::JoinSet. - Node.js integration occurs via stdio IPC, where
src/evm/fast-broadcast.tsspawns the Rust process and exchanges JSON-encoded requests and responses. - The
broadcast_racefunction inrust/fast-broadcast/src/lib.rs(lines 104‑166) deduplicates endpoints and returns the first successfuleth_sendRawTransactionresponse. getRaceUrlsinsrc/evm/multichain.tsprovides the endpoint list, while skills like the Odos swap (src/evm/odos.ts) conditionally invoke the race when multiple RPCs are configured.- This architecture achieves sub-millisecond latency for HFT arbitrage while keeping the Node.js event loop free for concurrent strategy execution.
Frequently Asked Questions
How does the Node.js gateway communicate with the Rust fast-broadcast binary?
The Node.js gateway spawns the Rust binary as a child process using child_process.spawn with piped stdio. It writes JSON requests to the child’s stdin and parses JSON responses from stdout. This stdin/stdout protocol allows the TypeScript wrapper to remain agnostic of the internal Rust implementation while maintaining minimal latency overhead.
Why is Rust used instead of native Node.js for the RPC race?
Rust provides a garbage-collector-free async runtime that can spawn thousands of concurrent HTTP requests without event-loop blocking. In HFT arbitrage, even a few milliseconds of GC pause or event-loop contention in JavaScript can cause a race loss. The Rust implementation in rust/fast-broadcast/src/lib.rs uses tokio::task::JoinSet to parallelize submissions across RPC endpoints deterministically.
What happens if all RPC endpoints reject the transaction?
If every endpoint returns an error, the broadcast_race function aggregates all failure messages into a single error response. The Node.js wrapper detects the !parsed.ok condition and surfaces this as a standard JavaScript Error, allowing the calling arbitrage skill to implement fallback logic or logging as needed.
How does the system determine which RPC endpoint won the race?
The Rust worker tracks the completion order of the JoinSet tasks. The first successful HTTP response to eth_sendRawTransaction returns its transaction hash and the corresponding RPC URL as the wonBy field in the BroadcastRaceResult. The Node.js gateway logs this latency and endpoint data for performance analysis while continuing with receipt monitoring.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →