Repository navigation
fix(shred-merger): dedup each version of a shred separately, keyed on the leader signature (BLO-48422 A9) - #193
Conversation
… the leader signature (BLO-48422 A9) The merger deduplicated on (slot, fec_set_index, local_index) alone. When a slot arrived in two versions, it emitted whichever copy reached it first at each index, so a set could mix both versions and leave a signature-checking consumer, such as the RPC node's TVU fed from 232.0.2.1-3 (BLO-46795), short of both. The production forwarder saw sets with two Merkle roots at the Alpenglow switch on 2026-10-21 13:54Z, when SIMD-0384 rolled back the slots after genesis block 459,652,999. The key now also carries the first 8 bytes of a version-4 frame's body: the leader's signature over the FEC set's Merkle root, which every shred of the set shares. It is not a hash of the whole shred, because the resigned last set of a slot ends in a signature that differs per retransmitter. Each version of a shred is emitted once. A version-3 frame has no signature and keys as 0, which matches any version, so mixed wire versions dedup as before. Later versions are counted in other_versions on the stats line and in shred_merger_other_versions_total. They go to every output except v3, so 232.0.0.2 still carries one version of each shred, the first. Only signature-checking consumers gain; v3 consumers and first-wins assemblers such as txfeed's can still build a set from two versions. Both history caps count later versions. The forwarders check no signature, so a flood of forged versions of one shred costs nothing; without the caps it would pass the per-slot cap and then the global one, which forgets real slots first. The maxDedupIDs note is re-measured for the new map types with go1.25: 24-38 B an id, 35-56 B a later version. shred/retention.go: the slot arithmetic in two comments now assumes ~212 ms slots. SIMD-0525's 200 ms step took effect from epoch 1053 (slot 454,896,000, 2026-10-09 14:40Z).
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex (applied directly in-run; no nested CLI).
Reviewed head: f81b88c
Looks good. I traced observe through every branch: known/same version, v3 wildcard, a repeated alt, an alt that resets the slot, and a new id past the global cap. d.ids stays equal to len(ids)+len(alt) on every path, including drop and the reset. frameVersion reads offset WireHeaderSize (28). That is where a v4 body begins with the 64-byte leader signature (shred/v3.go:34-36). It returns 0 for v3 and bodyless frames.
Critical Issues (0)
Important Issues (0)
Suggestions (2)
- [native-codex]
cmd/shred-merger/main.go:476—ver == 0doubles as the v3 wildcard. So a v4 frame whose signature starts with 8 zero bytes also becomes a wildcard. If such a frame (e.g. forged on ingest B) arrives first, every real version at that position is aduplicate. That is no worse than today's first-wins behaviour for any forged first shred, so this does not block. If you want the wildcard to mean only "no signature", one option is to passb[0]==3as its own flag instead of overloading 0. - [pr-review-toolkit:comments]
cmd/shred-merger/main.go:817— the finalsignallog line still prints noother_versions, while the periodic stats line now does. This is cosmetic.
Strengths
- Both caps count later versions, and
TestDedupBoundsVersionsOfOneShred/TestDedupBoundsVersionsAcrossSlotsshow it. This closes the forged-version flood that the uncorrected variant would have let through. known = falseon the per-slot reset is pinned byTestDedupSlotResetByALaterVersion, which was a real mutation survivor.- Keeping later versions off the v3 output preserves the one-version-per-shred contract for 232.0.0.2 and W6. Keying on the leader signature rather than the whole shred correctly ignores the per-retransmitter resign.
- The deploy plan comes with a concrete, falsifiable check:
ignoring(ingest), plus the measured 0.866-0.897 ratio band.
Recommended Action
- No blocking changes requested.
- Merge once the remaining required CI checks finish green.
|
Landed by kkroo drive session a2176509 at |
Why
The CT 140 shred-merger deduplicated on
(slot, fec_set_index, local_index)only. When a slot arrives in two versions, the union emitted whichever copy reached it first at each index. A set could then mix both versions and leave a signature-checking consumer short of both. That consumer is the RPC node's TVU, fed from 232.0.2.1-3 (BLO-46795).The production forwarder saw FEC sets with two Merkle roots at the Alpenglow switch on 2026-10-21, when SIMD-0384 rolled back the slots after genesis block 459,652,999 (block time 13:54:00Z). Its conflicting_roots counter, which counts a
(slot, fec_set_index, index, is_coding)seen with a second Merkle root (libmmtshred-forwarder/src/dup_suppress.rs:199-201), read 0 through 13:53:59Z and 1,249 at 13:54:04Z. It still read 1,249 at 2026-10-24 23:30:37Z. The dedup history so far is tracked in BLO-48422, item A9.What changes
ver: the first 8 bytes of a wire-v4 frame's body, which is the leader's signature over the FEC set's Merkle root (frameVersion,cmd/shred-merger/main.go:475). Every shred of a set shares it.observe(main.go:390),emitOther, emits a later version once.other_versionson the stats line and asshred_merger_other_versions_total.first_from_listener2now includes the later versions listener2 contributes, anddup_prodanddup_listener2count only their repeats. No script parses these fields.shred_merger_*series, so it needs no change.ver0, which matches any version. A shred both inputs deliver is still emitted once. Both ingests are on wire v4 now: the production forwarder runs--wire-version v4, and so does CT 140's listener2. That is what lets the key separate versions.send(main.go:625) gives them toall,data,coding-evenandcoding-odd, never tov3. So 232.0.0.2, which feeds the players and W6, still carries one version per shred: the first.main.go:434), so its copy from the other listener is a duplicate.maxDedupIDsnote now uses go1.25 measurements:shred/retention.go. "Two slots back" is now ~424 ms, and 2 s is about nine slots.Who gains
Only signature-checking consumers, such as a validator TVU, gain. The v3 output and first-wins assemblers, such as txfeed's (
txfeed/assemble.go:241counts a second copy as a dup), can still build a set from two versions.Not done
Tests
Format, vet and tests, go1.25.0:
gofmt -l ./cmd/shred-merger/ ./shred/: no output.go vet ./cmd/shred-merger/ ./shred/: clean, with cgo and withCGO_ENABLED=0.go test ./cmd/shred-merger/ ./shred/: ok, with cgo and withCGO_ENABLED=0. 28/28 pass in shred-merger.go test -race -tags purego ./cmd/shred-merger/ ./shred/: ok.New tests:
TestDedupKeepsEachVersionOfAShredTestDedupV3FrameMatchesAnyVersionTestDedupBoundsVersionsOfOneShred: a one-key flood of maxSlotIDs+10 forged versions.TestDedupSlotResetByALaterVersion: the version that starts its slot over returnsemit, notemitOther, and its repeat is a duplicate.TestDedupDropCountsLaterVersionsTestFrameVersionReadsTheLeaderSignature: includes a frame cut right after the signature's first 8 bytes.TestDedupBoundsVersionsAcrossSlots: later versions alone reachmaxDedupIDs.TestSendKeepsLaterVersionsOffTheV3OutputChanged tests:
checkIDscounts later versions.observecalls passver0.sendcalls passlater=false.shred_merger_other_versions_total.Mutation-checked: 15 single mutations, 11 by the implementer and 4 in review, each now caught by at least one test. The implementer's set includes the uncorrected variant, in which later versions return before both caps. The two that survived the review are caught by the new tests:
known = falsefrom the per-slot reset (main.go:434) failsTestDedupSlotResetByALaterVersion: "version past the cap: 3, forgotten=1; want emit, 1".len(b) >= hdrLen+8to>(main.go:476) failsTestFrameVersionReadsTheLeaderSignature.Deploy
Merging deploys nothing; the drive lands it once Ally and the required checks pass at the head. Deploying the binary to CT 140 is Omar's go, following the BLO-44853 precedent:
/usr/local/bin/shred-mergertoshred-merger.pre-a9.cmd/shred-mergerat the merge commit and install it.systemctl restart shred-merger.service./metricsexportsshred_merger_other_versions_total, andrate(shred_merger_other_versions_total[10m])is about 0. The production forwarder's conflicting_roots has read 1,249 since 2026-10-21 13:54:04Z, so real later versions are rare. A rate in the thousands per second means the two ingests key the same shred differently, and every shred goes out twice on every output but v3.rate(shred_merger_emitted_total[10m]) / ignoring(ingest) rate(shred_merger_received_total{ingest="a"}[10m])stays within about 0.03 of its own value over the 10 minutes before the swap. (ignoring(ingest)is required:receivedcarries aningestlabel thatemittedlacks, and without it the division matches nothing.) Over the 24 h to 2026-10-24 23:30Z this 10-minute ratio ranged 0.866-0.897, and two windows 20 minutes apart differed by at most 0.029; it read 0.8752 at 2026-10-25 00:06Z.emittedby its absolute rate. Over the same 24 h its 10-minute rate ran from 1,581 to 6,004/s (the 30-minute rate, sampled every 2 h, from 2,601 to 4,980/s).Rollback: restore
.pre-a9and restart.