Conversation
…llow A storage audit binds, in round 1, a nonced root over each pointer record a node holds, and round 2 must serve the bytes that reproduce it. The node kept only the record the last update replaced, so two paid updates to a pointer between the rounds made an honest holder fail round 2 with DigestMismatch, a confirmed failure that feeds the trust penalty. An owner who is also one of the holder's auditors knows exactly when its round 1 has been answered, so this was a cheap way to penalise a chosen honest neighbour. The round-1 session now keeps the root reported for each pointer leaf, the pointer store keeps every record an update replaces rather than only the last one, and round 2 serves the one record, held or replaced, whose root matches. Replaced records are kept for ten minutes, longer than a round 1 over the largest subtree an auditor waits for plus the session its round 2 must arrive within. Roots are capped at 65,536 across all live sessions, the oldest sessions giving theirs up first. When a node cannot serve the record round 1 read, because it aged out, was evicted, or the session kept no root and the pointer has been updated since, round 2 is rejected as Transient instead of guessing: no trust penalty, only the credit of that audit. A node that holds nothing at all for the pointer is still reported absent, as before. The auditor, the wire format and the subtree-audit protocol id are unchanged. ADR-0017 records the change and amends one point of ADR-0016, which is left as written.
Review of the first version found three ways a node could still lose the record an audit was owed. - A flood of pointer-heavy round-1 sessions made older sessions give up their roots to make room, so an honest holder's round 2 went transient. A round 1 whose roots do not fit now withholds its proof, exactly as a round 1 refused for capacity already does, and no live session gives its roots up. - Keeping a replaced record could evict another one before an update that then failed. Eviction now waits for the rename to succeed, and a failed rename takes back the record it kept. - A round 2 could read the new record before the one it replaced was kept. The replaced record is now kept before the rename, under the same lock. ADR-0017 now states what a transient round 2 costs (the auditor forgets the holder's standing for the whole pinned commitment, with no trust penalty) and that the ten-minute retention is sized for the default configuration.
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.
Two paid updates to a pointer between the two rounds of a storage audit made an honest holder fail round 2 with
DigestMismatch, a confirmed failure that feeds the trust penalty. ADR-0016 accepted that case. This PR closes it, and records the change in a new ADR-0017 rather than editing ADR-0016.Round 1 reports, for each pointer leaf, a nonced root over the record the node holds. Round 2 has to serve the bytes that reproduce it. The node kept only the record the last update replaced, so after two updates neither the record held nor the one kept was the one round 1 read.
Now:
Transient. That is the auditor's timeout lane: no trust penalty, but the auditor forgets the holder's standing as a proven holder for the whole pinned commitment until it passes again. A node that holds nothing at all for the pointer is still reported absent.The auditor, the wire format and the subtree-audit protocol id are unchanged.
Linear issue
Closes V2-1277
Risk tier
Compatibility
PointerStore::supersededreturns every replaced record kept, newest first, asBytes, instead of the last one;handle_subtree_slice_challenge_with_pointerstakes the round-1 pointer bindings;PointerBindingsandpointer_bindingsare new. None of these exists in a released ant-node, since pointers have not shipped.Semver impact
Test evidence
mainat4f78154:several_updates_between_the_rounds_do_not_fail_an_honest_holder(three paid updates between the rounds, judged by the auditor's ownverify_slice_response) returnedFail(DigestMismatch). It passes with this change, and asserts round 2 serves exactly the record round 1 read.round_two_serves_the_record_round_one_read_across_several_updates(e2e) sends round 1 and round 2 by hand over QUIC to a live node's engine, with three updates to every opened pointer in between. It passes with this change and fails when the engine stops handing round 1's roots to the session, which I checked by making that one-line mutation and running it.Transientwhile the pointer is held andKeyAbsentonce it is not; a missing root isTransient, updated or not; round 1 binds exactly its pointer leaves; a session over the roots budget is refused and no opened session loses its roots or its place, even with the session cap full; the store evicts the globally oldest replaced record, and a record kept for an update that then fails costs no other record; retention outlasts the slowest audit, computed from the default config and the largest legal subtree.cargo test --lib --features test-utils1,236 passed;e2e111 passed, 3 ignored as onmain;migration_reclaims_disk2,migration_crash_safety5,migration_shared_volume5,storage_scale2,webrtc_direct_devnet3,poc_commitment_audit_attacks19,poc_audit_handler_live16,poc_bootstrap_stall3,poc_shutdown_lmdb_drain1, andpointer_convergence15, all passing.cargo clippy --all-targets --all-features -- -D warnings,cargo fmt --check,cargo docwith--deny=warnings, andscripts/adr-governance.pypass.New dependency
none
ADR
https://github.com/grumbach/ant-node/blob/fix/pointer-audit-retention/docs/adr/ADR-0017-pointer-audits-serve-the-record-round-one-bound.md
Mitigation / rollback
Revert. Nothing is persisted, so a node running the previous code simply serves pointers as ADR-0016 describes again.