Skip to content

fix(shred-merger): dedup each version of a shred separately, keyed on the leader signature (BLO-48422 A9) - #193

Merged
kkroo merged 1 commit into
mainfrom
kkroo/a9-merger-version-dedup
Oct 25, 2026
Merged

kkroo merged 1 commit into
mainfrom
kkroo/a9-merger-version-dedup

Conversation

@kkroo

@kkroo kkroo commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

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 (libmmt shred-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

  • Dedup key. The key adds 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.
    • It is not a hash of the whole shred. The resigned last set ends in a signature that differs per retransmitter.
  • Each version is emitted once. A new verdict from observe (main.go:390), emitOther, emits a later version once.
    • It is counted as other_versions on the stats line and as shred_merger_other_versions_total.
    • On the stats line, first_from_listener2 now includes the later versions listener2 contributes, and dup_prod and dup_listener2 count only their repeats. No script parses these fields.
    • pve1's textfile collector passes through every shred_merger_* series, so it needs no change.
  • v3 frames match any version. A wire-v3 frame has no signature, so it keys as ver 0, 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.
  • Later versions skip the v3 output. send (main.go:625) gives them to all, data, coding-even and coding-odd, never to v3. So 232.0.0.2, which feeds the players and W6, still carries one version per shred: the first.
  • Both history caps count later versions.
    • Per slot, 65,536 ids. A later version that starts its slot over becomes that shred's first version (main.go:434), so its copy from the other listener is a duplicate.
    • Across slots, 2M ids.
    • The forwarders check no signature, so a flood of forged versions of one shred costs nothing. If the caps did not count later versions, such a flood would pass the per-slot cap and then fill the global one, which forgets real slots first.
  • Memory note re-measured. The maxDedupIDs note now uses go1.25 measurements:
    • 24-38 B per id;
    • 35-56 B per later version;
    • so up to about 75 MiB at the 2M cap, or about 112 MiB (56.0 B x 2^21) for a flood of versions.
  • Comment fixes in shred/retention.go. "Two slots back" is now ~424 ms, and 2 s is about nine slots.
    • SIMD-0525's 200 ms step took effect from epoch 1053: slot 454,896,000, 2026-10-09 14:40Z. The feature activated at slot 454,464,000 (epoch 1052, 2026-10-08 06:28:01Z), but getBlockTime shows epoch 1052 still ran at about 268 ms a slot to its end.
    • Slots now run at ~212 ms: getRecentPerformanceSamples gave 8,498 slots in 1,800 s (211.8 ms a slot) at 2026-10-24 23:25Z.

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:241 counts a second copy as a dup), can still build a set from two versions.

Not done

  • Pinning the v3 output to one version per set. One forged first shred would black out the set. Add it only once signatures are verified.
  • The erasure scorer's 2-slot window (W6). It is unchanged here.

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 with CGO_ENABLED=0.
  • go test ./cmd/shred-merger/ ./shred/: ok, with cgo and with CGO_ENABLED=0. 28/28 pass in shred-merger.
  • The CI race lane, go test -race -tags purego ./cmd/shred-merger/ ./shred/: ok.

New tests:

  • TestDedupKeepsEachVersionOfAShred
  • TestDedupV3FrameMatchesAnyVersion
  • TestDedupBoundsVersionsOfOneShred: a one-key flood of maxSlotIDs+10 forged versions.
  • TestDedupSlotResetByALaterVersion: the version that starts its slot over returns emit, not emitOther, and its repeat is a duplicate.
  • TestDedupDropCountsLaterVersions
  • TestFrameVersionReadsTheLeaderSignature: includes a frame cut right after the signature's first 8 bytes.
  • TestDedupBoundsVersionsAcrossSlots: later versions alone reach maxDedupIDs.
  • TestSendKeepsLaterVersionsOffTheV3Output

Changed tests:

  • checkIDs counts later versions.
  • Existing observe calls pass ver 0.
  • Existing send calls pass later=false.
  • The metrics test pins 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:

  • Deleting known = false from the per-slot reset (main.go:434) fails TestDedupSlotResetByALaterVersion: "version past the cap: 3, forgotten=1; want emit, 1".
  • Changing frameVersion's len(b) >= hdrLen+8 to > (main.go:476) fails TestFrameVersionReadsTheLeaderSignature.

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:

  1. Copy /usr/local/bin/shred-merger to shred-merger.pre-a9.
  2. Build cmd/shred-merger at the merge commit and install it.
  3. systemctl restart shred-merger.service.
  4. Ten minutes after the restart, check:
    • /metrics exports shred_merger_other_versions_total, and rate(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: received carries an ingest label that emitted lacks, 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.
    • Do not judge emitted by 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).
  5. Add a row to W6 change-points.md.

Rollback: restore .pre-a9 and restart.

… 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).
@allyblockcast

allyblockcast Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

🔗 Paperclip issue: BLO-44853
🔗 Paperclip issue: BLO-46795
🔗 Paperclip issue: BLO-48422

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 == 0 doubles 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 a duplicate. 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 pass b[0]==3 as its own flag instead of overloading 0.
  • [pr-review-toolkit:comments] cmd/shred-merger/main.go:817 — the final signal log line still prints no other_versions, while the periodic stats line now does. This is cosmetic.

Strengths

  • Both caps count later versions, and TestDedupBoundsVersionsOfOneShred / TestDedupBoundsVersionsAcrossSlots show it. This closes the forged-version flood that the uncorrected variant would have let through.
  • known = false on the per-slot reset is pinned by TestDedupSlotResetByALaterVersion, 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

  1. No blocking changes requested.
  2. Merge once the remaining required CI checks finish green.

@kkroo
kkroo merged commit 7c8ba58 into main Oct 25, 2026
24 checks passed
@kkroo

kkroo commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Landed by kkroo drive session a2176509 at f81b88cc: Ally 0C/0I at that head, drive-merge-gate PROCEED, one squash merge with a trailer-free message -> 7c8ba589.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant