Skip to content

feat(gateway): matched-capability snapshot digest v2 (board N20) - #440

Merged
LamaSu merged 1 commit into
masterfrom
feat/n20-matched-digest-v2
Sep 30, 2026
Merged

LamaSu merged 1 commit into
masterfrom
feat/n20-matched-digest-v2

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Summary

Board row N20 (composition): the versioned matched-capability snapshot digest.

  • The content is gateway's decision on 9/24 (#2354).
  • The byte layout was drafted by composition and acked by gateway (#3547), with gateway's counter and conditions folded in.
  • The steward ruled that v2 rides accepted-deal v3 (#3533, #3536).
  • The full spec is in the reconciliation plan (plan-accepted-deal-v3.md, section "N20").

This PR adds matchedCapabilityDigestV2, its pre-image and geohash to packages/gateway/src/services/matched-capability-digest.ts. v1 is unchanged, and nothing calls v2 yet. v3 wires it into R10 and seals a per-node matchedCapabilityDigestVersion.

What v2 commits to

v1 leaves these out:

Key Why
operatorSettlementAddress The address R10 pays, from the same resolver, so the commitment and the payment can never diverge. Anything other than 0x + 40 hex throws, with no fallback, because email-owned kernels exist.
csd.url + csd.contractDigest The versioned url AND the content digest, from resolveCapabilityContractIdentity. A version alone misses in-place edits (8d7fc56 rebound the print leg under /v1).
measurementProfile {id, version}, or explicit null until R21.
kernelLocationGeohash6 The kernel's registered location, never the capability row's (see #437). R10 enforces no request location constraint, so a kernel that moves re-quotes once.
priceMinorUnits, currencyDecimals An exact integer of minor units, never a float.
  • Tiers are the SERVED set, which is gateway's counter: the declared tiers clamped to the kernel's authorized ceiling by the one resolver R10 admits a tier with. The caller passes them in.
  • The pre-image has exactly 12 keys. Nothing from the input is spread in, every field is read once, and a value that breaks its rule throws rather than being coerced.
  • The geohash uses only IEEE comparisons and halving, so every language computes the same cell. Longitude comes first, and >= the midpoint gives 1.

Tests

  • The golden vector, byte for byte: 540 bytes, 0x7558a7e581e369241ac072f29f0b60a4d6d6e1de757180f12fa7750e969d29bf. An independent Python re-implementation computes the same.
  • The geohash: known cells (NYC dr5reg, Sydney r3gx2f), the boundary rule, and throws for a value that is not a location.
  • The board's N20 negative: every included value moves the digest. That is all 12 keys, each CSD field alone, the profile's fields, and a kernel move across a cell.
  • Normalization: address case, tier order and duplicates, and the digest's hex case give one digest. A move inside a cell does not change it.
  • Nothing outside the 12 keys reaches the commitment.
  • 42 fail-closed cases.
  • v1 is unchanged and never equals v2.
Suite Result
v2 and v1 digest tests 19 passed
full gateway 2935 passed, 6 skipped, 0 failed

The capture and capture-3d suites fail to load without a built @pcc/verifier dist; CI builds first.

Mutations: 14 of 15 killed, each read from its counts. Killed:

  • no lowercasing;
  • no zero-address check;
  • unnormalized tiers;
  • no content-digest lowercasing;
  • no domain;
  • geohash precision 5;
  • > instead of >=;
  • an undefined profile accepted;
  • no price bound;
  • no bigint check;
  • the input spread into the pre-image;
  • a loose CSD url;
  • a default cell for a bad location;
  • no tier range.

The survivor is equivalent: it drops the typeof half of the decimals check. Number.isInteger already rejects every non-number without coercion, and the "decimals as a string" case exercises it.

Open

  • Gateway's ack of the 12th key, the kernel geohash (#3558). If gateway counters, only that key changes.
  • Review: gateway (the module's owner) on resume. A small cross-family pack follows.

🤖 Generated with Claude Code

Adds matchedCapabilityDigestV2 and its pre-image next to v1, which is
unchanged. v2 is the versioned snapshot digest from gateway's decision
(#2354). Composition drafted the byte layout and gateway acked it (#3547).

v2 commits to what v1 leaves out:
- the address R10 pays;
- the CSD's versioned url AND content digest;
- the measurement profile (null until R21);
- the kernel's registered location, as a precision-6 geohash;
- the price as an exact integer of minor units, never a float.

The pre-image has exactly 12 keys. Nothing from the input is spread in,
every field is read once, and a value that breaks its rule throws rather
than being coerced. The geohash uses only IEEE comparisons and halving, so
every language computes the same cell.

Nothing calls v2 yet. Accepted-deal v3 wires it into R10 and seals a
per-node matchedCapabilityDigestVersion (steward #3533, #3536). The 12th
key (the kernel geohash) awaits gateway's ack (#3558).

Tests:
- the golden vector byte for byte: 540 bytes, 0x7558a7e5...29bf, also
  computed by an independent Python implementation;
- geohash cells and the >= boundary;
- every included value moves the digest; normalization; nothing outside
  the 12 keys reaches the commitment; 42 fail-closed cases.
- Full gateway suite: 2935 passed, 6 skipped. The capture suites do not
  load without a built @pcc/verifier dist.
- Mutations: 14 of 15 killed. The survivor is equivalent: dropping the
  typeof half of the decimals check, where Number.isInteger already
  rejects every non-number.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@LamaSu
LamaSu marked this pull request as ready for review September 30, 2026 19:17
@LamaSu
LamaSu merged commit 3a74686 into master Sep 30, 2026
5 checks passed
LamaSu added a commit that referenced this pull request Oct 1, 2026
… follow-up, confirmation round)

astra's confirmation review of bb742a2 reopened 440-A as a MEDIUM. The tier
count check refused only exactly 0, so a Proxy reporting a negative length
passed it. The index loop then ran zero times and an empty tier set was
committed. bb742a2 had removed the post-loop non-empty check on the
premise that the count was already above zero, but the code did not
guarantee that premise. Reproduced first: the reviewer's Proxy with length
-1 did not throw at bb742a2.

The count must now be at least one. That makes the premise hold by
construction, so the loop pushes or throws at least once. A test covers
lengths -1, -16 and 0.

Tests: matched-capability-digest v2 19/19, plus the v1 and decomposer
digest tests (34 in all). Mutations: reverting to === 0, and dropping the
lower bound, are both killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu added a commit that referenced this pull request Oct 3, 2026
fix(gateway): #440 review follow-up: v2 digest tier and location handling (N20)
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