Skip to content

feat(pointer): store, serve and pay for mutable references - #231

Open
grumbach wants to merge 30 commits into
mainfrom
feat/pointers-immutable-owner
Open

grumbach wants to merge 30 commits into
mainfrom
feat/pointers-immutable-owner

Conversation

@grumbach

@grumbach grumbach commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Linear issue

Closes V2-1277

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

A new stored record type with its own payment identity, replicated, committed, audited and priced like a chunk.

Compatibility

  • Wire: additive. The pointer messages from feat(pointer): paid mutable references with an immutable owner ant-protocol#34, appended to ChunkMessageBody. Six pointer replication messages are appended to the replication enum, so every earlier discriminant keeps its value; an older peer cannot decode them, so a node only ever asks peers that have sent it a pointer message, and never penalises silence.
  • Audit: the subtree audit's protocol id moves to v2, because round 2 carries a new slice item for pointers. As with ADR-0009, old and new releases do not audit each other across the upgrade.
  • Storage: pointers live in their own store under {root}/pointers/<shard>/. The chunk store is untouched, and a chunk-only node's commitment root does not move. The persisted commitment retention keeps its format; which leaves are pointers is written to a sidecar beside it.
  • Browser: the WebRTC-direct listener admits pointer GET and paid PUT through chunk_protocol, and HELLO advertises pointer_protocol when the node has a pointer store.
  • API: additive. One behaviour change for existing callers: a chunk PUT is refused if a pointer occupies that address (and vice versa), rather than one kind silently overwriting the other.

Semver impact

  • breaking
  • feature
  • fix

Test evidence

  • cargo test --lib --all-features: 1227 passed.
  • cargo test --test pointer_convergence --test poc_audit_handler_live --test poc_commitment_audit_attacks: 15, 16 and 19 passed.
  • E2E over real QUIC (--test e2e): pointer_replication 12 passed; replication 49 passed; subtree_audit and first_audit passed.
  • cargo clippy --all-targets --all-features -- -D warnings and cargo clippy --all-features -- -D clippy::panic -D clippy::unwrap_used -D clippy::expect_used: clean.
  • cargo fmt --all -- --check, RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps and scripts/adr-governance.py: all clean.
  • Real Chromium against a seven-node ant-devnet built from this branch, with on-chain payment on Anvil (the ant-client browser suite): a pointer is created, updated, pointed at a second pointer, read back by a client that wrote nothing, and resolved to its chunk.

Adversarial coverage:

  • Forks. Every delivery order of a record set converges to one value, checked exhaustively over all permutations, including on a node started empty and one restarted from disk. A fork held by two nodes is healed on both by any later counter.
  • Counters. Any paid state that beats the held one is taken, including a jump to u64::MAX; anything older is refused as stale.
  • One payment, many writes. 64 valid signatures over one paid state yield one stored record and one file on disk.
  • Unpaid work. A resubmission and a stale arrival are refused before any signature check, and admission runs before the signature too.
  • Cross-kind confusion. The paid cache is keyed by a typed chunk or pointer target, and a pointer quote never consults the chunk store.
  • Replication. A paid PUT to one node reaches its whole close group; an update replaces the old state everywhere; a node that missed an update or joined late is brought level by neighbour sync; a state only one node holds is not adopted; an unpaid fresh offer is refused; the possession check penalises only the member that dropped the record; pruning needs the close group's proof, and never deletes a pointer a retained commitment holds.
  • Audits. A node holding its committed pointers passes, over the wire. A node that dropped them fails. A relay that answers round 1 without the bytes fails, as does one serving another replica's copy of the same state or another owner's record. An update between the rounds does not fail an honest holder. A pointer leaf is accepted only at a pointer's exact length.
  • Retention. A commitment holding pointers survives a restart with its exact pin. An older release reading the same file keeps every chunk-only commitment a peer pinned.
  • Browser boundary. Pointer replies pass the sanitizer with errors and payment refusals redacted; a full record fits the small response bound.

Known pre-existing failure, unrelated and not caused by this branch: poc_price_floor_live::enforced_floor_rejects_cheapest_of_k_real_settlement fails identically on an untouched checkout of main.

New dependency

None.

ADR

https://github.com/WithAutonomi/ant-node/blob/feat/pointers-immutable-owner/docs/adr/ADR-0016-pointers-immutable-owner.md — docs/adr/ADR-0016-pointers-immutable-owner.md, added by this PR.

Mitigation / rollback

Pointers are opt-in at construction: a node built without a PointerService refuses pointer messages cleanly and does not advertise them to browsers. A rollback keeps every chunk-only commitment answerable: the retention file keeps its old format, and only commitments that include pointers, which the older release could not answer anyway, are dropped. The subtree audit id change means old and new releases stop auditing each other until the upgrade is through; that pause is the only coordinated part of the rollout.

grumbach and others added 22 commits September 24, 2026 14:26
Pointers (ADR-0015) are mutable, owner-signed references addressed at
BLAKE3(domain || owner_key). The record and its wire messages live in
ant-protocol; this is the node's half.

Storage. The chunk store answers "already have it" and stops, which is
right for an immutable record and drops every update for a mutable one,
and it requires BLAKE3(content) == address, which no pointer satisfies.
Pointers get their own store: merge-on-put under one lock, the rename as
the commit point, an exclusive directory lock, and an index dropped for
any address whose file stops validating so a repair is accepted rather
than answered "unchanged".

Payment. A quote is paid against the record's state_id and must be issued
by the close group around its address — two different values with two
different jobs, carried as PaymentTarget { routing, content }. Chunks pass
the same address for both, so their behaviour and cache entries are
unchanged. The paid cache is keyed by a typed Chunk vs PointerState: a raw
32-byte key would let a client store a chunk crafted to sit on a pointer's
entry and buy its update at chunk price.

One payment buys one increment. A create is counter 0 and a client update
is exactly one past what the node holds, so an owner cannot pay once and
jump the counter. Replication keeps the counter order instead, so a
replica behind a gap can still catch up.

Admission runs before the signature check — capacity, cross-kind refusal,
and responsibility for the pointer's address — so a forged record for
someone else's address buys no ML-DSA verification.

Audit. The commitment leaf gains a record kind, bound by hashing pointer
leaves under their own domain, so a chunk leaf cannot be relabelled to
escape the bytes_hash == key guard. Chunk-only roots are bit-identical.
Pointer leaves are refused at round 1 until round 2 serves a whole record:
a peer signs its own commitment, so it could otherwise name any key with
the hash of cheap bytes it holds.
as_bytes became to_bytes upstream: the record no longer caches its
encoding, because a fixed-width layout has exactly one, so it re-encodes
from the fields instead. The ADR now shows the record as the five fields
it is, and says plainly why the key is carried — ML-DSA has no key
recovery and a 1,952-byte key cannot be a 32-byte address.
Three simplifications, all deletions.

The kind-tagged audit leaf goes. Nothing in production ever built a
pointer leaf — commitment rotation reads the chunk store only — so
LeafKind, pointer_leaf_hash, build_of_kinds and the per-leaf wire tag were
machinery for a case that cannot arise. Removing them also puts the
subtree audit family back to v1: the tag changed a positionally-encoded
leaf, which would have paused mixed-version audits through a rollout for
no benefit. The audit format is now untouched by this work.

Prepared and PreparedPut go. PreparedPut was a one-field wrapper around
Pointer re-exposing accessors Pointer already had, and Prepared mirrored
Inspected arm for arm. Inspected gains a Verified arm and commit takes a
Pointer directly, so there is one enum where there were two plus a
wrapper.

PaymentTarget and PaidKey merge into one enum that is both the routing
decision and the cache key. Being an enum is the load-bearing part: a raw
32-byte cache key would let a client store a chunk crafted to sit on a
pointer's entry and buy its update at chunk price.

The ADR drops to Proposed while the PR is open, and its defence table now
says which rows are local guarantees and which still need replication —
fork convergence across the network is the client's doing until nodes
forward pointers to each other.
bytes_hash, all_keys, all_states, missing_states and holds_state described
commitments and version-aware sync that this PR does not build, and the
conditional GET they served has gone from the wire with them. Nothing
called any of it outside its own tests.

The ADR listed bytes_hash as a third identity binding 'this node's
commitment', which was false in the same breath as the section saying
pointers take no part in commitments. Two identities now: one that routes,
one that authorizes payment.
The index is a claim about a file, and both of inspect's early answers --
"unchanged" and "stale" -- assert the node already holds something at least as
good as what arrived. Answered from the claim alone, a lost or corrupted file
made the client count an acknowledgement for a record nobody could serve. The
file is now read back before either answer, and a failed read disowns the entry
so the submission lands as the repair it is.

Also:

- A pointer quote no longer consults the chunk store. A chunk sitting at a
  pointer's state address could set already_stored, and a majority of nodes
  saying "no payment needed" would strand the write as unpaid.
- IndexEntry carries the held PointerState rather than three fields copied out
  of it, so merge order and the paid increment are the protocol's own rules
  applied to the held state -- is_successor_of now has one definition and one
  caller instead of two implementations.
- Inspected::Verified, Inspected::state() and prepare() are gone; the tests
  that used them drive the same three steps production does.
- tests/pointer_convergence.rs lacked the test-module lint allow every other
  integration test carries, so clippy --all-targets -D warnings failed on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It cancelled the commit with a one-nanosecond timeout and then asserted the
cancellation had happened. On a fast runner the commit finishes inside any
timeout, so the assertion fired and the CI unit-test job went red while the
same test passed locally. It now polls the future exactly once -- that poll
hands the write to a blocking task and returns Pending, always -- and drops it.

Also:

- Inspected loses its nested outcome: Unchanged(state) / Stale(state) /
  Candidate. The handler reads the address and state off the variant instead
  of re-parsing the record, which deletes state_of, both "parsed then failed
  to re-parse" internal errors, and the arm for a no-op outcome that inspect
  could never return.
- The store module doc described a two-step put through prepare(), which no
  longer exists -- rustdoc's broken-link check would have failed CI on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Disowning the index entry for a record whose file had gone left the address
looking untouched -- and an address nothing is known about admits only a
counter 0 record. So the repair added in the previous commit worked at creation
and nowhere else: a pointer that had ever been updated could never be restored.

An entry now records that it is no longer on disk rather than disappearing. The
node stops serving it and stops counting it, but it still knows what it had,
and admits anything at least as good as the lost state -- the merge rule
replication would use to catch a replica up. A repair skips no payment: the
state it carries was paid for and the rest of the group already serves it.

Also:

- The read-back compares the whole state, not just the address. The index and
  the disk are written under one lock, so a valid record for the same address
  that is not the one the index names means the answer would describe something
  this node cannot serve.
- inspect runs on a blocking thread. It reads a record back off the disk, and a
  flood of arrivals must not put a blocking read on a runtime worker each.
- The ADR no longer claims write and read use one group "by the same call" --
  they are the same definition, but each does its own lookup -- and now says
  plainly that a majority of dishonest peers is not defended against.
- PaymentTarget::is_single_address, which only its own test called, is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A directory cannot be opened as a file on Windows without backup semantics, so
the post-rename directory sync failed on every write there. It was the Windows
unit-test job's only failure on this branch, and the warning it logged was not a
durability warning -- it was the wrong question asked of the wrong filesystem.
The sync is now Unix-only, where a directory entry is a thing you can flush;
NTFS orders the rename's own metadata.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comments still described BLAKE3 over a domain prefix and the preimage that
construction allowed; the identities are derive-key outputs now, and reaching
one from a chunk takes a collision across two BLAKE3 modes rather than content
anyone can write down. The typed paid-cache key does not rest on that either
way, which is the point of it being typed.

The ADR now also states two things it was quiet about: a read returns a state
only when two answering peers name it, because one peer serving an owner-signed
state nobody paid for would otherwise be believed by every reader; and a node
that loses a record can repair it while running but not across a restart, where
a missing file leaves nothing to remember. That, and a node joining a close
group after a pointer exists, are the same missing mechanism -- replication.

Also removes the empty test left behind when is_single_address was deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y claims

The ADR contradicted itself about the hash spaces on consecutive lines: it said
neither identity was reachable from any chunk content, then said nothing proved
the two ranges disjoint. Only the second is true. What derive-key buys is cost,
not impossibility.

It also understated where the defence ends. A read needs two peers to name a
state, so one peer cannot decide what a pointer says -- but two colluding ones
can, and since only the owner can sign, what that buys is the owner's own
updates unpaid. Raising the bar only raises the number of nodes to grind: the
pointer's address follows a key the owner chooses and node ids are choosable
too, so an owner determined to sit beside their own pointer reaches any fixed
threshold. Replication and audits are what actually answer it, and neither is
built. The table says so now.

And a read no longer necessarily ends at the answer quorum -- it keeps asking
while the states it has seen are uncorroborated -- so the consequence that said
it did is corrected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…can show

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the handler

The paid gate required exactly counter + 1, so a node holding one state refused
another at the same counter even though both were separately paid and the merge
rule says which wins. Give two nodes those states in opposite orders and each
keeps a different record for ever -- the fork the merge rule exists to prevent,
put back by the gate standing in front of it. The convergence tests never saw
it: they drive the store directly, below the gate.

The store now asks is_paid_update_of, which is "wins the merge and does not
skip", and two new service tests drive the real request handler: two nodes given
the same two states in opposite orders agree, and a counter jump is still
refused after a tie-break has been taken.

Also corrects two docs that claimed more than the code checks. get serves the
file rather than the index, so a file replaced by a different validly signed
record for the same address is served -- the reader verifies it and the read
quorum decides between replicas that disagree. And the held-state check parses
the body, so corruption inside the signature passes it and is caught on the
next read; verifying there would put ML-DSA in front of the payment gate, which
is the one place it must not be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two corrections, both mine.

The ADR and the admission comment still said every update is exactly counter + 1
after the rule changed to admit a separately paid tie-break winner at the
counter already held. What holds now is that one payment buys one state and at
most one increment: a tie-break moves the pointer without advancing the counter,
but it must strictly descend in target bytes and each step is bought on its own
state_id, so it buys nothing an ordinary update would not.

And PaymentTarget had been inserted between VerificationContext's doc comment
and its enum, so the long explanation of admission paths documented the wrong
type and VerificationContext was left with a one-line stand-in. PaymentTarget
now sits above that block with its own doc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADR-0015 was taken by direct browser clients over WebRTC-direct while this
branch was open, and the governance check refuses two ADRs wearing one number.
Renames the file, its title, and every reference to it across the node.
The rebase carried this branch's old Cargo.lock forward, which quietly
downgraded dozens of transitive dependencies main had moved on from. It is
now main's lock with one entry repinned: ant-protocol, for the pointer
record and its wire messages.
Repinning ant-protocol with cargo update also re-resolved an unrelated
Windows edge, putting winapi-util back on windows-sys 0.48.0 where main had
moved it to 0.61.2. The lock is now main's with the one ant-protocol entry
edited by hand, so nothing else moves.

Also adds ADR-0016 to the index, and records there that browser clients
cannot reach a pointer yet and what opening that path would take.
Pointer requests and responses were counted as Other, the bucket for
non-request and unknown future variants. That is what every other request
kind avoids, and it makes paid pointer traffic indistinguishable from
messages the table does not understand.

Both request kinds and all eight response outcomes now have their own keys,
on a third summary line because group 2 is already near tracing's field cap.
Unchanged and Stale are itemised apart from Success, since a re-submission
and a lost race are exactly what you want to tell apart when counting paid
writes. The classifiers are exhaustive -- the pointer response enums are not
non_exhaustive -- so a new variant is a compile error here rather than
silently becoming Other.
A pointer PUT asked check_capacity(), which asks whether a write of *zero*
bytes would fit and charges nothing, and then wrote 5,303 bytes. The file
store names that race itself: dozens of handlers can each pass against one
cached measurement before any of them has written a byte, and collectively
cross the configured reserve. Chunks avoid it by reserving; pointers did not.

Pointer writes now take a reservation for the size a record actually is and
hold it until the write lands, settled by net growth -- a create commits the
charge, a replacement or a no-op releases it, because the disk only grows in
the first case. The early admission check asks about the same size rather
than about nothing, so a client still learns a full disk before paying.

Three tests: a full disk refuses and stores nothing; eight concurrent
creations each take and settle a charge; and sixty-four writes that change
nothing give their charges back, which is the drop path -- a charge stranded
there would be permanent, and enough of them would make an empty disk look
full until the process restarted. The narrow race itself, where the reserve
is crossed between check and write, cannot be driven deterministically
without a test hook into free-space measurement; these cover the paths, not
the interleaving.
Two holes in the reservation, both found by review.

The charge was settled in handle_put while commit moved the write into a
blocking task. Drop that future after it spawns and the charge is released
while the detached closure goes on to publish the file -- exactly the case
the Reservation type says it exists for. It now moves into commit and is
settled inside the transaction that owns the write.

And settling by observed net growth could under-count, not only over-count as
I claimed. The check read the index before the rename; a file the index
claims can be gone, so what looked like a replacement grows the disk after
all, and the charge was released for bytes that landed. Every write that
lands now commits its charge. Over-counting is the safe direction and the
next measurement corrects it.

Staged bytes that cannot be removed keep their charge rather than releasing
it, since they are still on the disk.

The test that claimed to cover the release path could not reach it --
Unchanged and Stale return from inspect, before any reservation is taken --
so it is replaced by one at the store level that loses a commit race, which
is the reachable way to hold a charge and write nothing.

inspect, verify and commit are pub(crate) now: they are the request path's
internal steps, put_bytes is the public one, and a public method cannot take
a crate-private reservation anyway.
Third site of the same hole. A failed write or fsync leaves a partial
temporary file; if removing it also fails, the bytes are still on the disk
while the charge for them was being released. stage now reports whether it
left anything behind, and the caller settles the charge when it did.

The reservation tests now assert on the capacity guard's own counters rather
than on a following reservation succeeding -- which it would, stranded charge
or not, on a disk with room. A losing commit race must leave written_since
and in_flight exactly where it found them; a write that lands must move its
bytes from one to the other; and eight concurrent creations must each be
charged with none left hanging. That needs a test-only view of the two
counters, which FileStore and ChunkStore now expose under cfg(test).

The full-disk test's comment claimed to cover the reservation. It does not --
it refuses at the admission check, before payment -- and now says so.
A record rounds up to the allocation unit, so a charge is 12,288 bytes for
5,303 bytes of pointer. The eight-write assertion compared against eight raw
record sizes -- which four charges already exceed -- so it would have passed
with half the writes uncharged. The single-write and replacement checks were
inequalities for the same reason.

All three now assert exact deltas against the charge itself, exposed under
cfg(test) beside the counters.

The stage doc also said every failure removes the temporary file, one line
above the field that reports when it could not.
main released 0.20.0 and moved off the git patches: ant-protocol 3.0.0,
saorsa-core 0.28.0, saorsa-pqc 0.5.2, evmlib 0.10.0 and saorsa-transport
0.37.0 all come from crates.io now, and [patch.crates-io] is gone.

The pointer work needs exactly one thing back: ant-protocol carries the
Pointer record and its wire messages, and the published 3.0.0 does not have
them. So the patch section returns with that single entry, pinned by rev to
the pointer protocol branch at 3.1.0 -- which satisfies the 3.0.0 requirement
main declares, being additive on top of it. Everything else stays on the
release.

No source change was needed: the node compiles against saorsa-core 0.28 and
the record still signs and verifies under saorsa-pqc 0.5.2.
@grumbach
grumbach force-pushed the feat/pointers-immutable-owner branch from be63fed to 9de2bf2 Compare September 24, 2026 05:39
A node now takes any paid record the merge rule prefers to what it holds,
whatever its counter, including a first record above 0 at an address it
knows nothing about. The +1 gate is gone.

It was the one thing that stopped a node catching up. A write ends once
five of the seven close-group peers answer and the rest are cancelled, so a
peer can miss an update; a peer that joins the group later holds nothing.
Under +1 each refused every later update for good, and three such peers
left no write able to reach its quorum -- after the client had paid.

One guard stays: a record this node lost is still remembered, so it takes
that state or anything newer back, and refuses anything older. A replay
cannot roll a node back just because its file went missing. A record that
loses is now answered Stale rather than PaymentRequired, which is what it
is.

Pins ant-protocol to 4412b6a, which drops is_paid_update_of and
is_genesis. ADR-0016 is updated to match: the counter orders states rather
than metering them, and a joining or lagging node is brought level by the
next update.
A pointer's copies used to be exactly the ones the client wrote: a member
the write missed, a node that joined the group later, or one that lost the
file stayed without the current state until the owner's next update. Now
pointers replicate through the same engine as chunks -- the same close
groups, sync rounds, churn triggers, quorum, pruning and possession rules --
but by state rather than by key, since a pointer's address is stable while
its state changes and replicas may hold different signatures of one state.

- Fresh: the node that accepts a paid state from a client offers the record
  with its payment proof to the rest of the close group; each receiver
  checks signature, responsibility and payment itself before storing.
- Repair: every sync round pushes hints (the states the receiver should
  hold) both ways. A receiver lacking a state, or holding an older one, asks
  the group which state each holds, adopts the best one a quorum hold
  exactly -- counted over the whole group, so a silent peer is never a
  vote -- and fetches it from a holder.
- Pruning: after the hysteresis a record out of range is deleted, at once if
  the node is far outside the group, otherwise only when all but one of the
  current group return a valid record at that state or newer.
- Possession: minutes after an offer, each member must produce the record or
  is penalised, as a chunk holder is.

Six appended replication variants carry this. Requests only go to peers
that have sent a pointer message, and hints go out every round even when
empty, so an older peer is never asked what it cannot decode and never
penalised for silence.

The store gains what this needs: sorted enumeration, delete with the disk
charge credited back, stats, 256 shards by last byte as the chunk store
keeps, a structure-only startup scan (reads verify anyway), and the chunk
store's Windows rename retry.

Tests: nine multi-node E2E cases over real QUIC (fresh reach, update
replacement, repair of a lagging node, a late joiner via the engine's own
loops, a lone state refused though its hints arrived, an unpaid offer
refused, possession penalising only the dropper, pruning with and without
proofs), the verdict function, and the store additions. The existing chunk
replication E2E suite passes unchanged (46/46).
A node now commits to the pointers it is responsible for in the same
signed storage commitment as its chunks, so they count toward its quoted
price and are spot-checked by the subtree audit.

- A pointer leaf is (address, pointer_leaf_hash(address)): the root binds
  which pointers are held, not their state, so an update never moves it.
- Round 1 reports a pointer leaf at the fixed record length; a node that
  lost one refuses, a confirmed failure. The auditor accepts that leaf
  shape only at exactly a pointer's length.
- Round 2 proves a pointer with its whole signed record, which the
  auditor verifies and checks belongs at the committed address.
- The subtree audit protocol id moves to v2 for the new slice item.
- Retention persists which leaves are pointers (format 2), written only
  when some slot holds one, so a rollback still reloads format 1.
- Pruning never deletes a pointer a retained commitment still holds.
Browser clients (ADR-0015) can now read and write pointers:

- The WebRTC-direct listener admits a pointer GET and a paid pointer PUT
  through chunk_protocol, each bounded by the small response size; a
  full record fits well inside it.
- Every pointer reply passes the sanitizer; errors and refused payments
  are redacted as for chunks.
- HELLO advertises pointer_protocol, so a browser never asks a node that
  would refuse.

Telemetry parity with chunks: pointer_put_rpc and pointer_get_rpc
latency events on the rpc_latency target, the disk pre-check refusal on
the disk_precheck target, and the pointer count on commitment rotation.
From an adversarial review of the pointer audit and browser work:

- A pointer audit could be passed by a node holding nothing, fetching the
  few sampled records once round 2 named them. Round 1 now binds a nonced
  root over each pointer record's bytes, as a chunk leaf does, and round 2
  must reproduce it; another replica's copy of the same state does not.
  The store keeps the record each update replaced for five minutes
  (bounded), and round 2 serves it beside the current one, so an owner
  updating mid-audit does not fail an honest holder.
- Retention wrote a new format whenever a slot held a pointer, which a
  rolled-back node could not read at all. The retention file is back to
  the pre-pointer format; pointer leaves go in a sidecar keyed by
  commitment hash, so an older release keeps every chunk-only commitment.
- HELLO advertised pointer_protocol even on a node with no pointer store.
A node already retries its own port, which clears a socket still being
released. It cannot clear a port the host will not hand out at all, as
happens on Windows runners, where "Failed to create transport" for node
0 has failed runs on main and on this branch in whichever test drew that
range. More tests starting networks means more draws, so the harness
now moves a network that cannot create a node to a fresh random range,
up to three times, and a test holds a port to prove it does.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant