fix(core): record a resolved transaction bucket key, so a stray of a spent group lands (C21) - #372
Open
vieiralucas wants to merge 9 commits into
Open
fix(core): record a resolved transaction bucket key, so a stray of a spent group lands (C21)#372vieiralucas wants to merge 9 commits into
vieiralucas wants to merge 9 commits into
Conversation
vieiralucas
force-pushed
the
core/c21-tx-bucket-rewrite
branch
2 times, most recently
from
July 29, 2026 03:54
68b99e7 to
bedb84b
Compare
…spent group lands (C21) A group's bucket key is `(author, TxId)` and committing it consumed the key with no record kept, so a later member of that key started a fresh bucket at an arrival count the group had already met and nothing could meet again. Which members committed and which were left holding was the arrival order's, so one op set folded to two states: three rewrites reach it — every member of a three-group declaring two, an unrelated op of the same author re-tagged with a live group's id, and one op id delivered under two envelopes, where `apply` dedups before reading the tag. `Document` now keeps a per-`(ClientId, TxId)` resolved-key set, written when a bucket commits and when a tagged arrival's id is already held under a different envelope, and read to untag a later arrival for that key so it merges standalone. It rides `encode_state` (STATE_VERSION 13) so a restart does not re-open the hole, and both projections drop it whole — a key names an author and a group, never a partition.
… the mint Three defects in the first pass, from a cold review: - the dedup branch read "not in the buffer" as "a different envelope", so every resend of an already-applied tagged op spent its key. A duplicate is ordinary traffic on a transport that retries, so state became a function of how often an op arrived — and a fabricated group id on such a duplicate grew the persisted record without bound. It now fires only where the buffer is holding that id under a *different* group, which is the only evidence a bucket can offer, and spends both keys rather than the arriving one. - the tag was read before `is_admissible`, so an envelope no replica may hold decided which groups this one released. Admissibility is judged first. - the author never buckets its own group, so it recorded nothing and held a stray every receiver merged. `tag_atomic` spends the key at the mint. Pins the fourth shape the record does not reach — two envelopes of one id naming different groups, order-dependent on main already — as its own test.
Second cold review, on the head of the first pass: - eviction untagged its buckets without spending their keys, so a member arriving after one waited on a group the replica had already released. Two replicas running one eviction policy then disagreed over nothing but which had ticked when the last member landed — a regression against main, where "both evicted everything" was a convergence point. Eviction is the fourth point a bucket resolves and now spends the key like the other three. - the dedup branch built its buffer lookup eagerly, so every resend paid a linear scan of an uncapped buffer (44x on a 2000-op buffer). It searches only for an id the buffer can hold. - the mint spent its key without draining, leaving a released member in the buffer until some unrelated arrival ran one. Four new lines had no test killing them: the record's sort (load-bearing — two replicas encoded different bytes without it), both projection scrubs, and the untag on decode. Each has one now. Files C28: unanimity is judged over the members that have *arrived*, so a unanimous subset can reach its count before the dissenter lands — a minority count rewrite folds one op set to two states. Pre-existing on main; the ARCHITECTURE sentence that implied otherwise is corrected rather than left.
…'s order Third cold review: HIGH nothing, and two of the previous pass's four "now covered" lines were still undefended — the second commit's own claim was wrong, because a slice-based edit had swallowed one of the tests it added. - `project_zones`' record scrub had no test; `project_read_paths`' did. A zone-scoped snapshot now has to bucket a stray of a withheld zone's group, and to stay the same size whether that zone resolved one group or nine. - `tag_atomic`'s drain had none. Restored and verified by deleting the line. - the record's ordering rested on two hash sets happening to disagree; it now folds the same groups in every rotation. Also corrects ARCHITECTURE and KANBAN C20, which still required an age input or an evict-only-what-was-waiting contract before a periodic tick could be safe. Eviction spending its key removed that: a replica that ticked between two members of an honest group and one that did not now read the same state and hold the same record. What remains is stated instead — a replica that never evicts still does not converge with one that does, and the buffer of untagged not-yet-ready members still differs by tick placement, so it is state and record a bare period preserves, not snapshot bytes.
…ecord's order Fourth cold review, plus the direction-blindness lesson from C24: a rule that is consistently wrong satisfies "two orders agree" perfectly, so every comparison the fix introduces was mutated to see whether a *named* test objects. - the record's sort direction was the one that did not. Reversing it passed the whole suite, because the only assertion was that rotations agree — and they agree in any order. The encoded section is now read back and asserted key-ascending. (The outcome-deciding comparisons were already pinned: the conflict guard by 3 tests, the stray untag by several, take_complete_tx's lowest-position rule by 1.) - four sites claimed the third closed shape covered "a destranded copy racing the tagged one". It does not: the record spends a key on evidence the buffer is *holding*, and a destranded copy carries no tag to disagree with, so an honest three-member group plus a bare copy of one member folds to 2 observable states over its 24 orders — no forgery, and identical on main. The closed shape is two envelopes of the *same* group; C27 is widened to own both ways a second envelope can go uncontradicted, and the (b) half is reachable on honest traffic. - the growth bound overstated itself in both clauses: one key per op is equal to, not smaller than, the dedup set, and a peer sending conflicting tags charges two per id. Restated, and pointed at C20's missing buffer cap.
… correctly Fifth cold review: HIGH nothing, three MEDIUMs. - judging `is_admissible` before the dedup is load-bearing and nothing pinned it — the previous commit claimed every introduced comparison was mutated, and this one was not. With the check moved back, a forged `count: 0` on a *held* member's id spends both keys and releases the honest group waiting under one of them: an envelope every replica refuses deciding what this one shows. Now killed by name. - the previous commit over-corrected the third shape to "two envelopes of the same group". Wrong in both directions: identical envelopes spend nothing, and the branch's own passing test closes a different-groups case. What the record closes is a second envelope that disagrees with the copy the buffer is *holding* — in group id or in declared size; what C27 owns is a second envelope with nothing tagged to disagree with. - "the record narrows C27 from a split in what a replica reads to a split in which keys it has spent" was false. On the orders that buffer the disagreeing copy the conflict rule fires and everything lands, so a pool that read two ways without the record can read three with it. Measured over 392 forged pools: better on 156, unchanged on 232, worse on 4; eviction still collapses every one. Stated as measured. Also: the record's sort ignored its client component (two clients collide on a TxId readily, since it hashes member seqs alone) — the test now folds two authors; both projection tests now complete the bucket with a member on its own key, so the reading tells "held" from "refused"; and the eviction tick claim is scoped to a caller that keeps ticking.
vieiralucas
force-pushed
the
core/c21-tx-bucket-rewrite
branch
from
July 29, 2026 06:13
b7e20a0 to
52586ec
Compare
Sixth cold review: HIGH nothing, one measured regression against main. The record is per-replica *evidence*, and ARCHITECTURE says every filtering seam destrands the survivors of a group it cuts — so the recipient that most needs the key is the one with no evidence to earn it. The author spends the key at the mint and a whole-delivery recipient spends it on commit, but a zone- or read-scoped recipient served the same group untagged never buckets it and never spends it. A forged stray under that group's id then merges at the first two and is held at the third, where on main all three held it alike. Attacker-triggered (it needs the forged stray this unit exists for) and closed by eviction, which is another reason C20 is a correctness policy. Filed as C46 rather than fixed here: the other way is for the destranding seams to spend the keys they cut, and a snapshot projection would first have to answer what it may reveal about a group straddling its cut — the naive `extend(split)` re-adds groups that fell wholly inside the withheld partition, which is exactly what the record's clear exists to hide. Also corrected: the growth bound named the buffer cap, but eviction empties the buffer and keeps the key — the bound is the dedup set; the conflict rule's credit went to the `buffered` short-circuit rather than to `buffered_tx`, which is the gate that decides; and a TxId collision within one author now costs the later group its atomic view rather than merely sharing a bucket.
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.
Closes KANBAN C21 — a convergence-law violation: two replicas that folded an identical op set read different state.
The defect. A group's bucket key is
(author, TxId). Committing a bucket removed its members from the buffer and kept no record, so a later member of that key started a fresh bucket at an arrival count the group had already met and no arrival could meet again. Which members committed and which were left holding was the arrival order's. Three rewrites reach it, none malformed on any member's own terms:x,y,zcommits{x,y}and holdsz;z,y,xcommits{y,z}and holdsxm1,m2,m4commits{m1,m2},m1,m4,m2commits{m1,m4}applydedups on op id before reading the tagThe fix.
Document::resolved_tx: HashSet<(ClientId, TxId)>; a member arriving under a resolved key is untagged and merges standalone. A key is spent at each of the three points a bucket can be:drain_buffer)tag_atomic) — the author applies its own edits as it makes them and buckets nothing, so without this it holds a stray every receiver mergesRides
encode_state(STATE_VERSION12 → 13). Both projections drop it whole: a key names an author and a group, never a partition.Deliberately not taken. The record does not fire on an id merely applied. A resend is ordinary traffic on any transport that retries, so spending a key there makes state a function of how often an op arrived — the same law broken in the other dimension — and a fabricated group id on a duplicate would grow the persisted record without bound.
is_admissibleis judged before the dedup, so an envelope no replica may hold decides nothing about the groups this one holds.Residue, pinned as its own test and filed as C27. Two envelopes of one id naming different groups, where the forged one completed another group's bucket and applied the id before the honest copy arrived. Order-dependent on
mainalready; the record narrows it from a split in what a replica reads to a split in which keys it has spent. Members converge on eviction.ARCHITECTURE.md§Opt-In: Atomic amended (the paragraph that named this shape open);DECISIONS.mdlogged.Summary by cubic
Records resolved
(author, TxId)bucket keys and untag late members so every arrival order lands the same ops (C21). Keys are spent deterministically on commit, the author’s mint, eviction, and when a buffered id shows up under a different group (spend both), and the record is key-ascending and persisted inSTATE_VERSION13.Bug Fixes
Document::resolved_tx; members under spent keys are untagged and merge standalone;decode_stateuntags-before-drain.is_admissiblebefore dedup.project_read_pathsandproject_zonesdrop the record to avoid leaking withheld partitions’ group counts. Clarified scope: the closed duplicate shape is a second envelope that disagrees with the copy the buffer holds (group id or declared size); identical envelopes spend nothing; destranded/tagless copies remain a residue; and a destranded recipient never earns the key a whole-delivery one spends (filed as C46). Residues tracked as C44, C45, C46.Migration
12→13; older snapshots are rejected. No API changes.Written for commit 9d1361c. Summary will update on new commits.