feat: gasless mode + external-chain load-testing correctness (chainId, confirmations, force-stop)#44
Open
IvanBelyakoff wants to merge 10 commits into
Open
feat: gasless mode + external-chain load-testing correctness (chainId, confirmations, force-stop)#44IvanBelyakoff wants to merge 10 commits into
IvanBelyakoff wants to merge 10 commits into
Conversation
External/prod privacy mode runs EXECUTION_LAYER=reth (preconf supported) but has no builder preconf socket (PRECONF_WS_URL empty) — the proxy is the only RPC endpoint. The receipt-polling fallback previously started only when !SupportsPreconfirmations, so these runs had zero confirmation sources and reported 0 confirmed even though txs landed on-chain. Start the chain poller whenever no live preconf stream is available: unsupported layer OR supported-but-no-URL configured. Verified: 200 TPS route-all test now reports 2725 confirmed / 0 failed (was 0 confirmed), confirmations sourced via eth_getBlockByNumber. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a gasless mode for load-testing chains that have zero gas fees and self-authorize senders by signature (eth_sendRawTransaction) — e.g. an external privacy proxy fronting a gasless devnet. No faucet keys or pre-funded/authorized accounts are needed. When enabled (StartTestRequest.Gasless from the UI toggle, or GASLESS env): - skip faucet funding and the 0-funded hard error (the blocker on gasless chains, where unfunded random accounts can't be funded); - skip the warm-start balance check (always reads zero on a zero-fee chain) and the builder-nonce reset (a bundled-builder concern, meaningless here); - send 0-value eth-transfers (sender needs no balance); - use zero gas tip and fee caps, skipping the gas-price/baseFee probes that would otherwise force a non-zero fee cap. Supports the eth-transfer transaction type only (contract types need a funded deployer); other types / realistic pattern are rejected with a clear error. The normal funding path is unchanged when gasless is off. Verified: gasless test skips funding, sets zero gas, reaches running and sends 0-value txs (rejected only on the local chain because its base fee is 7 wei > 0; accepted on a true base-fee-0 chain). Normal non-gasless test still funds and confirms (regression check: sent 1001 / confirmed 981 / 0 failed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The load generator signs every transaction with cfg.ChainID (default 42069). Against an external chain (route-all / gasless mode) whose id differs, every signature is invalid for that chain, so the chain rejects all sends — silently, counted only as "failed" with no surfaced error. Observed on B3 (chainId 1485275539): reads/auth/nonce-init all succeeded through the proxy, base fee is 0 and eth_sendRawTransaction is permitted, yet 100% of sends failed purely because they were signed for chain 42069. Query eth_chainId at the start of each test (after the route-all client is wired with the current token) and adopt the chain's reported id, overriding the configured default with a warning. On the bundled chain the reported id equals the default, so this is a no-op there. Best-effort: on query failure the configured id is kept. Removes the need for operators to know a third party's chainId when load-testing their proxy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In external/gasless mode there is no block-metrics WebSocket (no builder, the proxy is HTTP-only), so the live charts (MGas/s, TPS, block count, total gas) were empty even though the test ran fine — those series are fed only by the WS handlers. The chain poller, meanwhile, already fetches every block via eth_getBlockByNumber for confirmation scanning and throws away everything but the tx-hash list. Record block metrics from the block the poller already has — no extra RPC. The recorder mirrors the WS handler (cumulative gas, per-period buffer, rolling windows), so it populates the same series the chart reads and leaves the getBlockMetricsViaRPC fallback dormant (blockMetrics non-empty => no double count). Block time is taken from the blocks' own timestamps, not wall-clock, so MGas/s stays accurate even when the poller scans several blocks in one pass while catching up. Verified (poller path, local chain): live MGas/s ~8, block count and total gas climbing each sample, confirmations unaffected. Previously all zero without a WS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… grace Two correctness fixes for honest tallies on remote/polled chains (e.g. an external privacy proxy fronting a gasless chain), where the bulk of reported "failures" were measurement artifacts, not chain rejections. 1) Shutdown-canceled sends are no longer "failed". When a test ends, sends that are still in flight return context.Canceled (the worker context is canceled). Over a high-latency remote proxy many sends are in flight at the boundary, so they dominated the failure count (e.g. 78/987). These were never rejected by the chain — they were aborted by our own stop. The send callback now detects errors.Is(err, context.Canceled) and classifies them as discarded (the bucket for "sent, not confirmed"), without RecordTxFailed or tripping the circuit breaker. Verified the error reaches the callback as the bare context.Canceled sentinel through the NoBatch -> HTTPClient -> Call path, so errors.Is matches. 2) Adaptive confirmation grace on the poller path. With a preconf WebSocket, confirmations are instant and a 3s grace suffices. Polling blocks over HTTP lags the head, so the tail of a run needs several block intervals to be seen or it lands in "discarded". When there's no preconf WS, scale the grace to 6x the observed block interval (clamped 6-30s) using the cadence recorded by the chain poller, instead of a fixed 3s. Net effect: "failed" reflects real chain/proxy rejections; late-but-successful txs are "confirmed" or "discarded", never "failed". Verified locally on the poller path: adaptive grace = 6s, fail = 0, no regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Separates per-tx success from throughput measurement. The on-chain verification window is the block range [testStartBlock, testEndBlock] (head at the moment sending stops), deliberately excluding later blocks so throughput isn't diluted by post-test drain. But that same cut left the tail — txs sent in the final moment that land a block or two later — reported as "discarded"/"pending" even though they succeeded (observed on B3: onChainTxCount 920 vs live-confirmed 747). After the grace period, look up each still-"pending" tx's receipt directly (eth_getTransactionReceipt, bounded concurrency), independent of the block window: a successful receipt reclassifies the tx as confirmed; no receipt (or a revert) leaves it discarded. A still-"pending" map entry was never touched by the poller, so its sent-time is intact and RecordTxConfirmed counts it. Logs a breakdown (confirmedLate / notOnChain / reverted / lookupErrors). Net: "discarded" means "actually never landed", not "landed just after our cutoff" — while TPS/MGas/s stay measured over the [start,end] window. Note: uses lg.l2Client, which on an external/route-all run is the proxy (a full node serving receipts). The bundled block-builder endpoint does not serve eth_getTransactionReceipt, so this is a no-op (lookupErrors) on the rarely-used bundled poller path; the target external/gasless path works. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A send can return "already known" / -32000 ALREADY_EXISTS when the RPC client's internal retry resends a tx whose first attempt already reached the node (common over a remote proxy: a 429/502/503/504 or lost response triggers a retry, but the identical, deterministic signed tx is already in the mempool/chain). The tx succeeded; only the duplicate resend errored — yet it was counted as failed (and, with receipt-resolution, also later found on-chain, double-counting). Detect it in the send callback (isAlreadyKnownTx) and treat it as a successful submit: commit the nonce (the tx used it and landed) and leave the tx pending for the confirmation/receipt path. Don't RecordTxFailed, don't trip the circuit breaker. Unit-tested against B3's exact error string and negatives (nonce too low / underpriced are NOT treated as duplicates). Net: "failed" now excludes both shutdown-canceled sends and already-known duplicates — it reflects only genuine chain/proxy rejections. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ence The dashboard Stop button and natural (duration) completion both called the same StopTest(), so a user Stop still ran the full post-test sequence — the confirmation grace period (now up to 30s on the poller path), per-tx receipt resolution, and on-chain verification — making Stop feel unresponsive. Split the two: - StopTest() (the interface method the Stop button calls) sets a forceStop flag and runs the stop. Force-stop skips the grace period, receipt resolution, and on-chain verification, so the test ends promptly with the live counts. - Natural completion (completionWatcher) calls the internal stopTest() without forceStop, so the full confirmation/verification still runs. Also: - The grace wait is now interruptible (sleepUnlessForced), so a Stop arriving mid-grace aborts it. - stopTest() uses CAS on `stopping` to run the sequence once; a concurrent Stop during natural completion just sets forceStop, which the in-flight sequence observes. forceStop is reset at test start. Verified locally: Stop mid-run reaches "completed" in ~0.1s with all three skip markers; natural completion still runs grace + resolution + verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The receipt-resolution loop used context.Background() per lookup and ran to completion via wg.Wait(), so a force stop (Stop button) arriving while we were already inside the loop only set the flag — the loop kept fetching every pending receipt. The grace was interruptible and the call-site skipped resolution if already stopped, but the in-flight loop itself was not abortable. Tie the per-lookup context to a cancelable resolveCtx watched by a goroutine that cancels it when forceStop is set; also skip remaining items at the loop head. So a Stop mid-resolution cancels in-flight lookups immediately and skips the rest. Logs an abortedByStop count. Verified by unit tests: a Stop mid-loop aborts in ~0.1s (17 canceled, 33 skipped) instead of running all 50 blocking lookups; a preset force stop makes zero lookups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NewLoadGenerator built the route-all privacy client eagerly and failed to start if the auth token file was absent. That's wrong for the standalone/paste flow: the token is pasted in the dashboard after the stack is up, and runInitialization already rebuilds the privacy client (reading the current token) at the start of every test. The bundled stack hid this by pre-writing a token, so it only surfaced standalone. Build the route-all client at startup if the token is already there; otherwise log a warning and fall back to plain clients, which runInitialization replaces once a token exists. One client instance is shared across builder/l2 (matching the route-all rebuild). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on the route-all/org-routing work (#41, #42) to make the load generator work correctly against an external chain via a privacy proxy (no bundled builder/preconf), and to make its tallies honest.
Gasless mode
feat(gasless): skip account funding, send 0-value eth-transfers, zero gas tip/fee caps (per-testgaslessflag orGASLESSenv) — for zero-fee chains that self-authorize senders by signature. eth-transfer only.External-chain correctness
auto-detect chainId: queryeth_chainIdand adopt it, so signatures are valid on the target chain (a stale default made every send invalid).poll chain for confirmations when no preconf WS: receipt/block polling fallback when the layer supports preconf but no WS is configured (external proxy).tolerate missing auth token at startup: in route-all, defer the privacy client (token is pasted later) instead of failing to start.Honest metrics (no successful tx mislabeled)
chart MGas/s & TPS from the chain poller: feed the live charts over HTTP from blocks the poller already fetches (no WebSocket) — zero extra RPC.don't count shutdown-canceled sends as failed+ adaptive confirmation grace (scaled to observed block time on the poller path).resolve still-pending txs by on-chain receipt at test end: late-but-included txs become confirmed, not discarded — throughput stays windowed, success is per-receipt.treat "already known" resubmits as sent: RPC-retry duplicates of an already-submitted tx are not failures.Force-stop
Pairs with the gasstorm PR (standalone stack + gasless dashboard toggle). Adds unit tests (
already_known_test.go,resolve_pending_test.go); full loadgen suite green.🤖 Generated with Claude Code