Skip to content

geoprobe-target: bound offset replay with a per-key slot floor - #4308

Draft
nikw9944 wants to merge 5 commits into
nikw9944/adhoc-413from
nikw9944/adhoc-527
Draft

geoprobe-target: bound offset replay with a per-key slot floor#4308
nikw9944 wants to merge 5 commits into
nikw9944/adhoc-413from
nikw9944/adhoc-527

Conversation

@nikw9944

@nikw9944 nikw9944 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary of Changes

  • geoprobe-target bounds offset replay with a per-signing-key MeasurementSlot floor: an offset more than 2 minutes below the highest slot that key has proven is dropped, as are repeats once the floor has stood still for 30 minutes.
  • A valid signature never expires, so a captured offset could previously be replayed into location_offsets — the table the public geolocation explorer aggregates — forever. geoprobe: reject forged, replayed and spoofed geolocation input #4271 closed the forgery half of RFC-16's mitigation here; this closes the replay half.
  • signature_valid no longer asserts a check that did not run, and MinCache.Update no longer restarts best's expiry clock on an equal RTT (matching the agent's own cache).
  • Fixes geoprobe-target: replayed offsets still reach location_offsets (no MeasurementSlot check) #4286

The floor comes from the offset stream, not a ledger clock. geoprobe-target holds no RPC connection by design — RFC-16 makes it example target software whose premise is verifying with no chain access — and an absolute check would couple it to the sending probe's RPC health, since #4271 lets a probe stamp a frozen cached slot during its own outage. MeasurementSlot is signed, so the highest slot a key has proven is a lower bound on real time a replay can repeat but not advance. CLAUDE.md records the invariant.

30m reads looser than the agent's 15m lag, but the steady-state number is tighter. A healthy sender's floor advances every ~5 minutes, so the real window is about 7 minutes. maxFloorStall only covers a probe riding out its own RPC outage, where a dropped measurement is lost permanently rather than deferred.

This does not make the ingest path trustworthy. VerifyOffsetChain checks the signature against the AuthorityPubkey in the same datagram, and this daemon has no allowlist of known probe keys, so any freshly generated keypair can still write attacker-chosen coordinates. Closing that needs the onchain geoprobe registry, which the lake indexer already ingests and this daemon deliberately cannot reach — target enforces freshness, consumer enforces identity. An issue covering both halves is being filed separately;

Rejections are logged, not counted. geoprobe-target has no prometheus metrics, so these Warn lines are the only signal that a sender stopped being ingested. Each carries a stable message plus separate reason (slot_regressed / floor_stalled), authority_pubkey, sender_pubkey, offset_slot, floor_slot and floor_age_seconds fields. A counter would be a reasonable follow-up.

Testing Verification

  • Every behavior change has a test confirmed to fail with the production change reverted, then restored — not inferred. Notably, repeats must stay acceptable while the floor is fresh: a strict > rule would drop nine of every ten legitimate offsets.
  • Two adversarial cases: a forged offset stamped with slot 2^40 claiming a real geoprobe's SenderPubkey does not lock that geoprobe out; and a sustained replay against a stalled floor stays rejected across repeated sweeps rather than outliving its entry and reseeding from itself.
  • make e2e-test RUN=TestE2E_Geoprobe was not run — it needs Docker and cEOS, unavailable here. Worth a reviewer's time before merge: this changes ingest acceptance, so a false rejection loses a measurement permanently rather than deferring it.
  • The changelog entry is a fragment at changelog.d/nikw9944-adhoc-527.md; the repo has no collation script, so it needs folding into CHANGELOG.md by hand.

A valid signature never expires, so an offset captured off the wire could
be replayed into location_offsets forever. MeasurementSlot is inside the
signed payload, so the highest slot a sender has proven is a lower bound
on real time that a replay can repeat but cannot advance: reject offsets
more than 2m below that floor, and reject repeats once the floor has
stood still for 30m. Derived from the offset stream rather than a ledger
clock because geoprobe-target holds no RPC connection by design.

Also stop asserting signature_valid=true when -verify-signatures=false
means nothing was checked, and stop MinCache.Update resetting best's
expiry clock on an equal RTT, matching the agent's own cache.

Refs #4286
Review found two ways the floor did not hold. SenderPubkey is signed but
unauthenticated, so a minted keypair stamping a real geoprobe's
SenderPubkey with a huge slot would have locked that geoprobe out of
location_offsets; floors are now keyed by AuthorityPubkey, the key
VerifyOffsetChain actually checks against. And a rejected offer did not
refresh lastSeen, so a sustained replay outlived its own entry and
reseeded from itself every TTL instead of once per restart.

Also give the floor its own retention constant rather than borrowing the
display cache's -max-offset-age, tie maxFloorStall to
geoprobe.SlotCacheTTL, and derive signature_valid from the check result
rather than from the flag.

Refs #4286
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