Conversation
A pointer state at u64::MAX is now final (ant-protocol): nothing replaces it, not even another final state whose target sorts first. That is what lets an owner hand a pointer's address over for good, by signing one last state that points at the new owner's pointer. The store, the admission gate, fresh offers, repair and hints all compare with the protocol's replaces(), so they keep whichever final state they took first with no code of their own. The merge rule alone leaves one gap: a node holding no final state takes any, so a former owner could still finalize again on a node that joined the group after the transfer, or lost its copy. So before a node takes a final state it does not hold, from a client or a fresh offer, it asks its close group which state each holds. A peer claiming a different final state is asked for the record, and if it verifies -- only the owner could have signed it -- the write is refused as stale, naming the state the group proved. A claim alone refuses nothing, so one peer cannot block a transfer. The look runs after payment is verified, only asks peers that have sent a pointer message, and is bounded at four seconds inside the client's store timeout; silence proves nothing. Two different final states can still exist if the owner races them to different nodes. Each node keeps its first; the client decides by the close group's majority. So the possession check no longer penalises a member holding the other side of such a fork -- it holds what the merge rule told it to -- and repair, between two final states a wide group backs at quorum, adopts the larger side rather than whichever answered first. with_pointers now takes the service and wires both directions, so the node and the e2e harness cannot attach one and forget the other. ADR-0018 records the decision and amends ADR-0016's merge rule.
…nges ADR-0016 was edited in place: a backlink, the terminal-counter conclusion, the merge rule and the ownership consequence. It is restored exactly, and ADR-0018 now lists the three statements of ADR-0016 it overrides at the final counter. ADR-0018 also claimed more than the rule gives. The former owner keeps the earlier record and the key, so it can sign a second final state at any time, not only in a race, and any node the first has not reached will take it. What holds is that no node gives up a final state it holds. The claim that flooding final states pays for every round trip was false while a verified payment is cached; the ADR now describes the proven-conflict memory and the bounded, per-address looks that make it true, and the restore of a node's own lost final state without a look.
… to replay, and safe to restore The finality look took claims as they arrived but fetched each claimed rival in turn. A peer that claimed a rival and then stalled its fetch held the look until its four-second budget ran out, and a look that runs out finds nothing, so one peer could let a second final state in. Each peer's claim and fetch now run as one pipeline, all at once, and the first proof wins. A verified payment is cached, so replaying one paid final state that lost cost the node a signature check and a round of questions every time. A proven conflict is now remembered, up to 16,384 addresses, and a replayed loser is refused before its signature is checked. Looks for one address wait their turn, at most 64 run at once, and one that cannot start within two seconds answers Busy: a PUT gets a retryable error and a fresh offer is dropped, neither taking nor refusing the state for good. A node that lost the file of a final state it held was made to look before restoring it, so a peer on the other side of a fork could keep it from restoring its own copy. The store now reports what it remembers, and restoring a remembered final state asks nobody. ReplicationEngine::with_pointers keeps the signature it has on main; with_pointer_service wires the service both ways. ant-protocol is repinned to the head of its companion change, which only corrects documentation.
The companion change added documentation only: where a final state is permanent, and that conflicts below the final counter are ordered.
…ne look among queued replays A proven final state was stored one per address, so a later look that proved the other side of a fork replaced the first proof, and the state the first look had disproved could then be taken if the group went quiet. Up to two proven states are now kept per address, and a second never replaces the first; two different ones refuse every final state there. Looks for one address took turns but did not share answers, so replays queued behind a clear look each asked the group again once it finished. A clear look now answers the same state for ten seconds without asking, on the runtime's clock, so a burst of replays costs one round as ADR-0018 says. The bounds live in their own type so they are tested without a network: eight queued checks make one look, and a proof answers every replay of the loser.
…s over the address's last byte A look that found no conflict was reused for the same final state for ten seconds, even when the write it cleared then failed: a rival that landed meanwhile could be let in by a retry that skipped the look. The answer is now forgotten as soon as that write does not land, on the PUT path and the fresh-offer path, and it is reused for two seconds, as long as a replay queued behind the look can have waited for its turn. Looks took turns by the address's first byte, but the addresses one node holds share their leading bits, so nearly all its looks shared one turn and a single slow look made the rest answer Busy. They take turns by the last byte now. ant-protocol is pinned at the head of its companion change, which corrected two more documentation claims.
…ween final states adopts neither A fresh offer whose commit came back stale, because a rival had landed first, was reported as stored, so the clear look that preceded it outlived a write that never happened. A commit that loses now counts as not stored, and the look is forgotten. Repair picked the larger side between two quorum-backed final states, but on a tie, possible in a group of eight with a quorum of four, the state that answered first won, and two repairing nodes could adopt opposite sides. A tie now adopts neither until the group settles. ADR-0018 no longer claims the look keeps every final PUT inside the client's ten-second timeout: it adds at most six seconds after payment is verified, and a slow payment check can still outlast the timeout.
…lent peers, and take only the proof claimed Repair adopted the larger of two quorum-backed final states without counting peers that had not answered, so in a group of eight one node seeing four against three with one silent peer adopted the four, while another seeing the reverse adopted the other side, for good. A final state is now adopted only if it stays strictly larger with every silent peer counted for its rival, seen or not yet seen. A peer that claimed one rival final state and served another had the served record accepted as proof. The record must now be the state the peer claimed.
…r another address as no answer Repair grouped state summaries by identifier alone and kept the first one's counter and target, so one dishonest peer naming a final state's identifier with a lower counter absorbed the honest votes for it as a non-final state and slipped past the guard for final states; the fetch then took the real final record, matching the identifier only. Votes now count together only for the same whole state, and the fetched record must equal the state the quorum backed. A summary about some other address was documented as no answer but counted as a peer that answered holding nothing, which could hide a silent peer that might tie two final states. It is now left out entirely; an explicit nothing-held answer still counts.
…and test the whole-state backing A rival final state refused by a node that had lost the file of its own final state was answered Stale naming zeros, since only a state on disk was named. It now names the state the node remembers, so the sender can see what won. The check that a fetched record is exactly the state a quorum backed is its own predicate with a test that fails if it goes back to comparing identifiers only. ADR-0018 now says that a node running with no replication, a devnet or one whose replication engine failed to start, takes a final state on the merge rule alone, as a node whose group cannot answer does.
…imed The look compared only the identifier of the record a peer served with the state it had claimed, so a claim pairing a real rival's identifier with another target was taken as backed by that rival. The record must now be the whole state claimed, as repair already requires, and a test fails if the comparison goes back to identifiers.
dirvine
left a comment
There was a problem hiding this comment.
APPROVE — reviewed 8f5af8d.
No blocking findings. Traced final-state preservation through PUT, remembered-state/file-loss handling, replication and final-state witness verification. The latest deltas correctly return the remembered identifier when rejecting a rival and require the fetched witness to back the entire claimed state, not just its identifier. Their regressions are included in the passing pointer tests.
Verification:
- Exact head:
cargo test --locked --all-features --lib pointer— 95 passed, zero failures. - Prior head e813b9c:
cargo test --locked --all-features --test pointer_convergence— 17 passed. The subsequent reviewed delta only tightens witness backing and adds its regression; do not treat the earlier convergence run as an exact-head run. - Companion client transfer E2E at its e813b9c node pin: two tests passed using local nodes and Anvil (handover and refusal of a second transfer after one node retained a final state).
Limits: ADR-0018 deliberately promises per-node final-state retention, not globally irrevocable ownership consensus. Churn, lost copies and mixed-version deployment remain important. Some current-head CI checks are still running/queued; this is not an all-green-CI or release sign-off.
Review scope: the three companion pointer-transfer PRs were read together. Independent GLM-5.2 review found no blockers on the earlier reviewed heads; I checked subsequent deltas directly and reran the relevant tests. Its cautions about mixed-version deployment and the lack of global ownership consensus are valid, documented limitations rather than demonstrated regressions. Codex CLI could not review because its credentials were revoked; it is not counted as a completed review. Additional source-review seats have not returned, so this is not a claim of full-panel consensus.
Linear issue
Closes V2-1354
Risk tier
This changes which pointer state a node keeps at the final counter, and adds a close-group round trip before a node takes a final state.
What
Pointer ownership transfer by final redirection (ADR-0018). The owner key never changes. The owner signs one last state at
counter == u64::MAX, pointing at a pointer the new owner holds the key to. Readers are redirected there by every node that holds that state, the address stays the same, and no node that holds it ever gives it up. WithAutonomi/ant-protocol#40 makes a final state final: nothing replaces it, not even another final state whose target sorts first. This PR pins that rev and does the node's half.replaces. They keep whichever final state they took first, with no code of their own.Stale, naming the state the group proved.ReplicationEngine::with_pointerskeeps its signature;with_pointer_service(&PointerService)wires both directions, fresh writes and the finality witness, and is what the node and the e2e harness use.Compatibility
PointerStateRequestandPointerFetchRequest.ReplicationEngine::with_pointer_service,pointer::FinalStateWitness,pointer::FinalityCheck,PointerService::attach_final_state_witnessandPointerStore::remembered. Behaviour: a node refuses a second final state withStalewhere it previously took one whose target sorted first.Semver impact
Test evidence
cargo test --lib --features test-utils: 1,250 passed. New tests, each of which fails when its fix is reverted (checked by reverting it):cargo test --features test-utils --test pointer_convergence: 17 passed. With one final state among the records every delivery order converges on it, exhaustively, and with two the first delivered is kept; a stored transfer is not displaced by a final state whose target sorts first, nor by any lower counter.cargo test --features test-utils --test e2e pointer_replicationover real QUIC: 15 passed. A transfer written to one node reaches the group and a paid final state whose target sorts first is refused by every node; a node that missed the transfer refuses a different one because a peer serves the one it holds, and still takes the group's own; the possession check does not penalise the other side of a fork and still penalises a member holding nothing.cargo test --lib --features test-utils1,250 passed;e2e113 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_convergence17, all passing.cargo clippy --all-targets --all-features -- -D warnings,cargo fmt --all -- --check,cargo docwith--deny=warningsandscripts/adr-governance.py: all clean.New dependency
None.
ADR
https://github.com/WithAutonomi/ant-node/blob/feat/pointer-ownership-transfer/docs/adr/ADR-0018-pointer-transfer-by-final-redirection.md:
docs/adr/ADR-0018-pointer-transfer-by-final-redirection.md, added by this PR (Proposed). It amends ADR-0016's merge rule at the final counter.Mitigation / rollback
Re-pin ant-protocol to the previous rev and revert this branch. No stored record changes shape. The only coordinated part is that nodes and clients should agree on the final-counter rule, and a mixed group degrades to reads by majority, not to lost data.