Repository navigation
fix(dlv): the correctness pass the close stack merged without - #681
Merged
Merged
Conversation
…the newer one The republish pass selects a batch before it makes a single network call, and every iteration below that awaits. A close committing in that window supersedes a row already in the batch and publishes its own bytes under the same .../latest mirror key. Replaying the stale row afterwards puts an older generation back on every member — a closed vault serves its birth anchor again — and the damage is permanent and invisible: upsert_artifact_publication_state declines to write to a superseded row, so nothing records that the fleet now disagrees with us, and list_unpublished_artifacts skips published rows, so the newer bytes are never re-sent. The module's contract is that a superseded row is never replayed. It is now honoured against current state rather than state at selection time, by re-reading each row immediately before it is sent. republish_rows is split out so a test can hand it a deliberately stale batch, which is exactly what the real pass holds after its own SELECT. The existing sweep test cannot catch this: its SELECT already filters superseded rows, so it passes whether or not the pass re-checks anything. Also deletes clear_accepting_member and KeyedPutFanout::accepting_member_ids, which have no callers.
…ly be loaded Two changes to the settlement-slot claim path. Attribution. A member answering Refused while naming OUR digest as the value it holds is holding our bytes, and that arm counted it without asking who answered — the one acceptance path that skipped the check its siblings apply. Quorum means a majority of DISTINCT members hold these exact bytes, so a node replying under another's id could contribute twice to a majority that does not exist. The current node never phrases a re-ack this way; the arm exists for a node that would, which is exactly the node whose attribution cannot be taken on trust. Refusals stay unattributed on purpose: counting one makes the claim fail closed as Contested, so it can only make us more conservative. The fake fleet gains that phrasing so the case can be reached. Dropping the check turns the new test red with accepted: 2 of 3 — one node counted twice. FrozenClaimEnvelope. SPHINCS+ signing here is deterministic, so a rebuilt claim is byte-identical to a replayed one: nothing on the wire, in a digest, or in a test can tell which happened. The guarantee therefore has to be that the machinery is absent rather than merely unused. The type has one constructor — load, which reads bytes already retained — and claim_settlement_slot accepts nothing else. A caller with a signing key but no retained bytes cannot obtain one at all, and making recovery re-sign would mean changing this type rather than quietly editing a call site. frozen_claim_envelope now reads back what it just retained instead of wrapping the bytes in hand, so a retention that silently failed surfaces before the claim goes out rather than at recovery. The tests go through storage exactly as production does: a test-only constructor taking raw bytes would hand them the very capability the type exists to withhold. can_sign asks whether signing is possible without binding a key, so a caller that must not hold signing material while doing the work that precedes signing no longer has to.
…pe has one home Call-site half of the FrozenClaimEnvelope boundary. resume_close_intents no longer binds a signing key. It asks can_sign() to decide whether there is any point starting, and the commit half — which genuinely must sign, because the terminal proofs did not exist when the close was interrupted — moves to finish_prepared_close. Keeping the key out of the claim half's scope is what makes "the parent claim is never re-signed" a property of the code rather than of the current author's discipline. The claim comes from FrozenClaimEnvelope::load, whose only input is the slot it names; nothing on this path can build or sign one. dlv_close_intent.claim_bytes is dropped, column included. The envelope lived in two durable places that could disagree, and settlement_slot_claim_local is the one that FrozenClaimEnvelope reads. Schema v5 is introduced within this stack and main is still on 4, so reshaping it here owes no version bump. The replay test's doc claimed more than the test observes. SPHINCS+ signing here is deterministic, so a rebuilt claim is byte-identical to a replayed one and no wire-level assertion can separate them. The doc now says what the digests do prove — every attempt carried ONE envelope, the one frozen with the intent — and states that provenance is enforced structurally instead.
Collaborator
Author
|
Full suite on the replayed branch: 1797 passed, 0 failed, 7 ignored, 597s. That is the whole |
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.
The three reviewed correctness fixes for the DLV close stack, replayed onto
main.#676, #677 and #678 merged at their pre-fix heads (
d4a12311,bac58261,429f4e20) while these commits were still local, somaincurrently carries the stack without them. Each commit is the one written and verified against its original branch. One conflict was resolved on replay, instorage_node_sdk.rs:mainnow has #677'sMemberClaimResultadditions surrounding the deadimpl KeyedPutFanoutthis deletes.main's content kept, the dead impl deleted.What is currently wrong on
mainA superseded artifact row is replayed over the newer one.
republish_unpublished_artifactsselects a batch before it makes a single network call, and every iteration below that awaits. A close committing in that window supersedes a row already in the batch and publishes its own bytes under the same.../latestmirror key. Replaying the stale row afterwards puts an older generation back on every member, and the damage is permanent and invisible:upsert_artifact_publication_statedeclines to write to a superseded row, so nothing records that the fleet disagrees with us, andlist_unpublished_artifactsskips published rows, so the newer bytes are never re-sent. A closed vault would serve its birth anchor to every trader, indefinitely.The existing
a_superseded_generation_is_never_sweptcannot catch this: its SELECT already filters superseded rows, so it passes whether or not the pass re-checks anything.republish_rowsis split out so a test can be handed a deliberately stale batch, which is exactly what the real pass holds after its own SELECT.One acceptance path counts without attribution. A member answering
Refusedwhile naming our own digest as the value it holds is holding our bytes, and that arm counted it without asking who answered. It was the only acceptance path skipping the check its siblings apply. Quorum means a majority of DISTINCT members hold these exact bytes, so a node replying under another's id contributes twice to a majority that does not exist. Refusals stay unattributed deliberately: counting one makes the claim fail closed as Contested, so it can only make us more conservative.The claim envelope has two durable homes. SPHINCS+ signing here is deterministic, so a rebuilt claim is byte-identical to a replayed one, and nothing on the wire, in a digest or in a test can tell which happened.
FrozenClaimEnvelopemakes the machinery absent rather than merely unused: one constructor,load, reading already-retained bytes, andclaim_settlement_slotaccepts nothing else.resume_close_intentsholds no signing key at all, askingcan_sign()instead, with the commit half moved tofinish_prepared_close.dlv_close_intent.claim_bytesis dropped, column included, leavingsettlement_slot_claim_localas the single home.Schema note:
CLIENT_DB_SCHEMA_VERSION = 5was introduced by this stack and no v5 database has shipped, so reshaping it here owes no bump.Mutation controls
a_row_superseded_after_selection_is_not_replayed_over_the_newer_onea_refusal_holding_our_own_digest_still_needs_attributionaccepted: 2 of 3, one node counted twiceThe attribution fix previously had no test at all. The new one needed a fake-fleet knob for the refusal phrasing, because the current node never emits that shape, which is the reason the arm cannot be taken on trust.
Local gate
cargo fmt --all -- --checkcleancargo clippy --all-targets -- -D warningscleancargo test -p dsm_sdk --libfull suite, result posted below