Conversation
A pointer's owner key cannot change, but what its address resolves to can be handed over for good: the owner signs one last state, at counter u64::MAX, pointing at a pointer the new owner holds the key to. Readers of the address are redirected there, and the address never changes. That only works if the last state stays last. Under the previous order a state at u64::MAX was still displaced by another at the same counter with smaller target bytes, so a former owner could grind a target and take the address back. replaces() now has a rule ahead of the other two: nothing replaces a final state, not even another final one. Below the final counter the order is unchanged. The price is that two different final states are unordered, so each node keeps whichever it took first. Only the owner can make that fork, only by racing two final states to different nodes, and only while no node holds a final state yet: once one does, no node that holds it takes another. Which side a reader believes is decided by how many of the close group hold each, on the client. Adds FINAL_COUNTER, Pointer::finalize, Pointer::transfer_to and transferred_to on both the record and its parsed state. finalize refuses to sign past a record that is already final, since that is how a fork is made.
The docs and changelog said the former owner had no move left once a node held a final state, and that a fork could only be made before any node held one. Neither holds. The owner keeps the earlier record and the key, so it can sign a second final state at any time, and a node that holds no final state takes whichever one reaches it first. What the rule guarantees is narrower: no node gives up a final state it holds. The docs now say that, say that the refusal to finalize a final record guards a caller rather than the network, and note that nothing on the wire tells the old merge rule from the new one. A test pins the limit: an earlier record finalized twice gives two final states, and neither replaces the other.
…verywhere A frozen pointer was said to be frozen for good, and a transfer to leave only the new owner able to move the pointer on. Both hold only at the nodes that hold that final state: the former owner can still finalize an earlier record elsewhere. Two different final states were also called the only fork the rule allows, while conflicts at a lower counter are forks too; they are the only unordered conflict, and the only fork no later state heals. The changelog's merge-rule summary now says it holds below the final counter, and one test is renamed to say what it checks.
…ks for good only at the end Two claims were still absolute. A transfer redirects a reader through the nodes that hold that final state, not every reader: after the owner finalizes an earlier record again, a reader asking the other side goes elsewhere. And a second final state is not the only way to fork a pointer, since two states at one lower counter split nodes too; it is the only fork no later state heals.
dirvine
left a comment
There was a problem hiding this comment.
APPROVE — reviewed c6ef10f.
No blocking findings. Final states are deliberately incomparable: ordinary counters retain deterministic merging, whereas a stored final state is not replaced by another final state. The API explicitly rejects updates/transfers from an already-final record, without claiming an owner cannot sign competing histories. Wire format and paid state identity remain consistent with the companion node/client changes.
Verification: cargo test --locked --lib passed: 109 tests, zero failures. All GitHub checks were successful at the last check.
Watch-out: accepting/decoding the unchanged wire format does not make old nodes enforce the new final-state semantics. Deployment must be coordinated across protocol, node and client; this approval does not authorise a live rollout.
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 the pointer merge rule that nodes and clients both apply. The change is one comparison, at the final counter.
What
A pointer's owner key cannot change. The address can still be handed over for good: the owner signs one last state at
counter == u64::MAX, pointing at a pointer the new owner holds the key to. Every reader of the address is then redirected to the new owner's pointer, and the address stays the same.Under ADR-0016's rule that does not stick. A state at
u64::MAXis still displaced by another state at the same counter with smaller target bytes, so a former owner can grind a target in about two tries and take the address back. This PR adds one rule ahead of the other two:Below the final counter nothing changes. At the final counter, two different final states are unordered, so a node keeps whichever it took first. Only the owner can create that fork: it still holds the earlier record and the key, so it can sign a second final state, and any node the first has not reached will take it, at once or later. Once a node holds a final state, no arrival moves it off. The node PR also has a node look at its close group before taking a final state, and the client PR reads the side a majority holds and reports forks.
replacesstays a strict partial order: it is never true both ways round, it is transitive, and it is total except between two final states.Also adds
FINAL_COUNTER,Pointer::finalize(refuses to sign past a record that is already final, since a second final state is how a fork is made; a guard against a caller's mistake, not against the owner),Pointer::transfer_to, andtransferred_toon both the record andPointerState.PointerError::CounterExhaustednow says the pointer is final, instead of advising a migration before the last counter.Companion PRs: WithAutonomi/ant-node#239 (ADR-0018, and the node's look before a final state) and WithAutonomi/ant-client#210 (transfer, finality check, majority reads). Both pin this branch's head by rev.
Compatibility
u64::MAX.FINAL_COUNTER,Pointer::finalize,Pointer::transfer_to,Pointer::transferred_to,PointerState::is_terminal,PointerState::transferred_to. One behaviour change:PointerState::replaces/Pointer::replacesreturnfalsewhen the held state is final. Nodes and clients must agree on it. A node still on the old rule lets a smaller-target final state displace the first, and nothing on the wire tells the two rules apart (the format version is still 1); see ADR-0018's mixed-fleet note.Semver impact
Pointers are not in a published release yet (3.0.0 has none), so the rule change lands on an unreleased surface.
Test evidence
cargo test --lib: 109 passed, 22 of them inpointer. New tests:transfer_tosigns at the final counter to the recipient and refuses to sign past a final record.cargo clippy --all-targets --all-features -- -D warnings,cargo fmt --all -- --check,RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-depsandcargo build --no-default-features: 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:
ADR-0018, added by WithAutonomi/ant-node#239. It amends ADR-0016's merge rule at the final counter.Mitigation / rollback
Revert the rule-0 line in
PointerState::replaces. Nodes and clients pin this crate by rev, so a rollback is a re-pin, and no stored data changes shape either way.