Repository navigation
perf: serve Curio-PDP read bursts without timeouts; reduce head lag - #196
Merged
Merged
Conversation
…ecord revert backtraces
…ns, backtraces, payable, sends) + handler flow tests
…argets, proxy revert backtrace
…om un-executed gas estimates
go-ethereum's websocket client (Curio's EthClient) pings after 30s without a client write and arms a 30s read deadline that only a pong clears. go-jsonrpc v0.10.1 swallowed pings without replying, so every idle Curio eth connection was torn down 30s later with 'read tcp ...: i/o timeout', failing every eth_call in flight on it.
Content- and state-keyed caches (blocks, actors per state root, EVM states, bytecode, storage slots, per-execution-state results) with singleflight in front, so a burst of identical or overlapping eth_calls on a fresh head does the state walk once. A head warmer replays the actors and slots read on the previous head at the new state root.
… a missing sibling parent A block at the head height that shares the head's parents (another block of the same tipset) was dropped at the height fence. The head stayed a one-block tipset, and every block of the next epoch, whose Parents list the full tipset, found a parent missing and was skipped: on calibration, where most tipsets have several blocks, head only advanced via backfill about one epoch behind the network. Curio's Chain page reports that as 'slow (55s behind)'. Siblings with identical parent-derived fields are now persisted and grow the head tipset through the same divergence and corroboration gates as any head adoption (Lotus semantics: the head is the heaviest tipset, grown as its blocks arrive). A head+1 block whose parent tipset contains a block we never received fetches exactly those parent headers (content-address checked against its Parents) instead of being skipped.
fix: eth_estimateGas executes the call and reports reverts (stop Curio broadcasting doomed txs)
docs(VERIFICATION-M2): correct revert cause, mark #193 fixed
# Conflicts: # CHANGELOG.md
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.
Stock Curio-PDP 1.29.0 using Lantern v1.10.1-rc1 as its only chain node on calibration (our soak, and the first external operator) has two symptoms:
failed to process empty PDP proving periods: failed to get next challenge epoch for data set N: chain: read tcp 127.0.0.1:x->127.0.0.1:<port>: i/o timeout. In our soak that was about 256 per 24 h with ~29 data sets, and ~15% of PDPv0_Prove / PDPv0_ProvPeriod attempts were retried.Sync Status: No, slow (55s behind)while Lotus on the same box is in sync.This PR fixes both. Root causes, Curio's own definitions and before/after numbers below.
Problem 1:
read tcp ...: i/o timeouton eth_callCurio's timeouts (v1.29.0):
ethclientover a websocket (deps/apiinfo.goGetEthClient:erpc.DialOptions(ws://.../rpc/v1); only the first configured endpoint is used).contract.EthCallOpts(ctx), a 10 s context (pdp/contract/ethcall.go,EthCallTimeout = 10 * time.Second). When that fires the error iscontext deadline exceeded, noti/o timeout.rpc/websocket.go: after 30 s without a client write (wsPingInterval) the client sends a ping and sets the connection read deadline 30 s ahead (wsPongTimeout); only a pong clears it. When it expires the read loop fails withread tcp ...: i/o timeout, the connection is dropped and every request in flight on it fails with that error, however fast the server is.Root cause: Lantern's RPC server is go-jsonrpc v0.10.1, whose websocket ping handler records the ping as activity and does not reply with a pong (v0.10.3 does). Every Curio eth connection that sat idle for 30 s between bursts was torn down 30 s later, and whatever eth_calls were in flight at that moment failed with exactly the error operators see. Curio's work is periodic with idle gaps, so this hits a steady fraction of calls.
Reproduced with go-ethereum's own client (
curioburst -ws-idle-probe: anewHeadssubscription stays open while the client writes nothing):Fix: go-jsonrpc v0.10.3 (answers pings with a protocol-level pong). Regression test
rpc/server/ws_keepalive_test.go.Burst latency (secondary). Before, every eth_call on a new head re-walked the state tree on its own, and a cold block was fetched once per caller. The eth_call layer now has:
Nothing is keyed by "latest": every entry is keyed by content address or by the exact state it was computed on, so a new head can never be served an older head's value.
Problem 2: "slow (55s behind)"
Curio's definition (
web/api/webrpc/sync_state.go,SyncerState):age = now - ChainHead().MinTimestamp();okifage < 1.5 * BlockDelaySecs(45 s),slow (<age> behind)ifage < 5 * BlockDelaySecs(150 s), elsebehind. A node at the tip shows ages of roughly 3 to 33 s (epoch timestamp plus propagation, up to one epoch). 55 s means one epoch behind.Root cause (Lantern side):
net/blockingestbuilt the head from gossip blocks one at a time and dropped every block at or below the head height (bh.Height <= curHead). So:allParentsPresentfailed; the inline backfill only fills epochs above the store head, so it did nothing and the block was skipped;Live evidence from the baseline run: Lantern head heights 4205317, 4205319, 4205321, 4205323, ... (burst log below), and a side-by-side sample: Lantern head 4205321 with 1 block while Glif was at 4205322 with 4 blocks; Lantern then jumped straight to 4205323.
It is not #180 (
ChainHeadreturns the header store head directly;latest = head-1only applies to eth block tags), not head validation or state fetch (the gossip head path is header arithmetic), and not the polling Sync interval (inactive bridge-off).Fix (
net/blockingest):Numbers
Local, calibration, a fresh
LANTERN_HOMEper build (init --network calibration --no-wallet,node-type pdp, bridge-off, loopback listener), same machine and time window. Before =origin/main(009d6d0), after = this branch.scripts/bench/curioburst: one shared go-ethereum websocket client like Curio's; per burst 30 live data sets x (getNextChallengeEpoch,getDataSetLeafCount,getChallengeRange) = 90 concurrent eth_calls, plus 8 receipt polls,eth_getBlockByNumber latest,eth_blockNumberandStateGetRandomnessDigestFromBeacon; 5 s client timeout; head age sampled every 5 s.A. Burst every 30 s for 10 min (as specified; includes the first burst on a node started seconds earlier):
In both runs all 30 errors per view come from the first burst on the cold node. Burst wall time before: 3.2 to 4.3 s on a new head, ~80 ms on a repeat (the head only moved every 60 s); after: 3 to 14 ms for all 19 later bursts (the 30 s phase lands after the head warmer finished).
eth_getBlockByNumber latestmax 3368 ms before, 14 ms after; receipts p50 6.8 ms before, 1.7 ms after.B. Head-triggered bursts for 5 min (a burst within 2 s of every head change, like Curio's chain scheduler; first burst skipped as warmup; both nodes side by side):
read tcp ...: i/o timeout(~60%)This is the case that matters for Curio: the burst lands on a state root nobody has read yet. Before, the 30
i/o timeouterrors are the websocket teardown from Problem 1 reproducing inside a normal burst. After, the first burst on a new head is bounded by the chain of dependent block fetches (state tree path, EVM actor head, storage KAMT path: about a dozen sequential Bitswap round trips), done once and shared by all 90 calls; every later call on that state is served from cache in milliseconds (table A).The long pole of an after burst is now
eth_getBlockByNumber("latest")on a fresh head: p50 4193 ms, one 5 s timeout in 9 (before: p50 4849 ms, 2 of 5 failed). It walks the receipts, events and messages of the newest executed tipset from the network and is unchanged here; it is under Curio's 10 s and is the next thing to look at.C. Head age (now - ChainHead MinTimestamp), during the 10 min runs:
Glif's own ChainHead trails by up to an epoch, so wall-clock age is the meaningful number; the after node was ahead of Glif in 66 of 121 samples and never behind it (before: behind in 33, level 60, ahead 29).
Correctness
latestblock number, runs all 90 views, re-readslatest(discarding the round if the head moved), then asks Glif for the same views at that pinned block number. Before: 90/90 identical (block 0x402b18). After: 90/90 identical (block 0x402b22).rpc/handlers/ethcall_perf_test.go): a new head with a new state root is never served the previous result; the same state root with a different execution tipset is not reused either (NUMBER/TIMESTAMP differ); reverts replay as reverts; the f410 hint is re-verified against the current state; bad bytes from the network are rejected and never cached.net/blockingest/sibling_test.go): a sibling completes the head and the next epoch adopts immediately without any backfill source; a competing block at the head height is neither stored nor merged; a closed divergence gate or failed corroboration stores but does not grow the head; a missed sibling parent is fetched and adopted; a fetched parent that disagrees on parent fields is rejected.CGO_ENABLED=0 go build ./... && go vet ./... && go test ./...: green.Review of the earlier work on this branch
Kept: the go-jsonrpc bump and test, the eth_call caches / singleflight / warmer, coalesced LRU touches in the persistent block cache (one Badger write per block per minute instead of per read), parallel prefetch of event AMT roots for eth block gas/bloom, opt-in pprof on the metrics listener (
LANTERN_PPROF=1, off by default), the bench tool. Changed: the in-memory block layer only holds blocks up to 16 KiB, 16384 entries (worst case 256 MiB; it was 256 KiB x 32768, an 8 GiB worst case); the bench verifies at a pinned block, samples head age and can skip warmup bursts. Kept with a caveat: the long-lived Bitswap session (one session remembers which peers serve chain state instead of broadcasting for every block); reasoned, not separately measured.Config.SessionPerGetrestores the old behaviour.Build
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath ./cmd/lanternat 74a2f6c: sha25658099ca6688fd954d65e13458ed0c60d829ea9c3317d9bc518a7ec39d5a98293.Open
StateGetRandomnessDigestFromBeaconat head-10 returned "beacon entry ... not found in walked tipsets" for some epochs in both runs (5/19 before, 3/20 after). Separate issue, not touched here.