Skip to content

[AI assisted] Plan G tests + fix: did:key, did:pkh, LEI vectors, multi-anchor uniqueness#17

Open
scourtney-godaddy wants to merge 3 commits into
feat/plan-g-anchor-integrationfrom
feat/plan-g-anchor-tests
Open

[AI assisted] Plan G tests + fix: did:key, did:pkh, LEI vectors, multi-anchor uniqueness#17
scourtney-godaddy wants to merge 3 commits into
feat/plan-g-anchor-integrationfrom
feat/plan-g-anchor-tests

Conversation

@scourtney-godaddy
Copy link
Copy Markdown

@scourtney-godaddy scourtney-godaddy commented May 16, 2026

Adds did:key and did:pkh as DID sub-methods, swaps the LEI fixture for real public LEIs, and adds a live multi-anchor test that registers the same FQDN under FQDN, did:web, and LEI in sequence. Includes a follow-up Plan F fix so verify-acme stops rejecting base-only registrations for missing Identity CSR.


The third and final Plan G PR. Three additions plus one fix:

  1. did:key resolver under internal/adapter/anchor/did/: pure
    offline DID method. Multibase prefix validation (base58btc
    z only), pure-Go base58btc decoder using math/big (preserves
    leading zero-byte semantics), multicodec varint parser (LEB128),
    JWK conversion for Ed25519 (multicodec 0xED) and secp256k1
    (multicodec 0xE7). secp256k1 point decompression runs through
    Tonelli-Shanks (p ≡ 3 mod 4 short path) so the JWK carries
    both x and y coordinates. X25519 (0xEC) and P-256 (0x1200)
    surface DID_KEY_TYPE_NOT_IMPLEMENTED to flag the migration
    boundary.

    Tests cover two W3C-cited Ed25519 vectors with x values
    verified by round-tripping through this resolver, all five
    DID_BAD_FORMAT and DID_KEY_* reject paths, multicodec
    varint single + two-byte cases plus truncation, leading-zero
    base58btc round-trip, length validation, secp256k1 happy
    path through the curve generator G.

  2. did:pkh resolver: CAIP-10 chain identifier parsing
    (did:pkh:<namespace>:<reference>:<address>), eip155 address
    validation (0x + 40 hex), ChainResolver interface stub for
    on-chain lookup. The interface mirrors the LEI resolver's
    GLEIFClient injection pattern: lexical validation works
    without a chain client; a production HTTP-RPC client lands when
    ERC-8004 testnet plumbing is in place. The Anvil pre-funded
    address (0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) is the
    canonical test address throughout, matching foundry/hardhat
    documentation conventions.

    Tests cover Sepolia, Ethereum mainnet, Anvil chain 31337,
    uppercase namespace lowercased, all DID_PKH_BAD_FORMAT cases,
    validateEIP155Address against good and bad inputs,
    DID_PKH_CHAIN_NOT_CONFIGURED boundary, fake-chain happy path,
    chain-lookup errors, empty-JWK rejection.

  3. Real public LEI conformance vectors at
    docs/tests/conformance/anchor-0c-lei/lei-public-examples.json
    carry 4 mod-97-verified LEIs (the GLEIF documentation example
    plus three real public LEIs from the GLEIF Global LEI Index:
    Apple, Microsoft, ECB, chosen to exercise alphabetic-prefix
    LOUs and varied body shapes), lowercase + whitespace
    canonicalization vectors, and 8 reject vectors covering empty,
    too-short, too-long, hyphen, embedded-space, special-character,
    and two perturbed-check-digit cases. The fixture file is
    language-agnostic; an external Rust or Python implementation
    can consume the same JSON to validate against the same data
    the Go implementation runs against.

The fix:

  1. Base-only uniqueness scope widened from agent_host alone
    to (agent_host, anchor_type). The pre-fix predicate caused
    a real bug: registering multi.example.com first under the
    FQDN anchor and then under DID rejected the second
    registration with BASE_ONLY_FQDN_TAKEN, contradicting the
    spec's ANS-0 §7 cross-anchor coexistence rule. The fix gives
    each (host, anchor_type) tuple its own uniqueness slot.
    Legacy "anchor unspecified" rows occupy their own implicit
    slot, distinct from any explicit anchor profile.

    Caught by a multi-anchor live scenario script that registered
    the same operational FQDN under all three anchor types in
    sequence; pre-fix the second and third calls returned 409.
    TestAgentStore_ExistsActiveBaseOnly_AnchorScoped pins the
    new behavior.

Stacks on #16 (Plan G integration) → #15 (Plan G core) → #14
#13#12. Merge order: #12#13#14#15#16 → this.

Test plan

  • make check (gofmt + golangci-lint + 90% coverage gate)
  • did:key W3C-cited Ed25519 vectors + secp256k1 round-trip
  • did:pkh CAIP-10 parsing (Sepolia, mainnet, Anvil 31337)
  • LEI conformance fixture-driven test (3 real public LEIs + perturbed-check-digit rejection)
  • Multi-anchor uniqueness scope (TestAgentStore_ExistsActiveBaseOnly_AnchorScoped)
  • Live multi-anchor scenario against the demo RA: same FQDN under FQDN + DID + LEI; V2 list returns 3 rows with distinct anchorResolvedId values

🤖 Generated with Claude Code

scourtney-godaddy and others added 3 commits May 16, 2026 16:09
Two test improvements per external (Grok) testing guidance:

1. did:key resolver (the second 0.B sub-profile, fully offline):
   - Multibase prefix validation (base58btc 'z' only).
   - Pure-Go base58btc decoder using math/big; preserves leading
     zero-byte semantics (each '1' character at the start of the
     suffix becomes a leading 0x00 byte).
   - Multicodec varint parser (LEB128) per the multiformats spec.
   - JWK conversion for Ed25519 (multicodec 0xED) and secp256k1
     (multicodec 0xE7), with secp256k1 point decompression via
     Tonelli-Shanks (p ≡ 3 mod 4 short path) so the JWK carries
     both x and y coordinates as required for kty=EC.
   - X25519 (0xEC) and P-256 (0x1200) explicitly stub with
     DID_KEY_TYPE_NOT_IMPLEMENTED so future profile work has a
     clear migration boundary.
   - Tests cover: 2 spec-cited W3C did:key Ed25519 vectors with
     verified x values, all five DID_BAD_FORMAT / DID_KEY_*
     reject paths, multicodec varint single + two-byte cases,
     truncation rejection, leading-zero base58btc round-trip,
     unsupported-key-type errors, length validation, secp256k1
     happy path through the curve generator G.

   This is the first ANS-0 profile that is fully offline: a CI run
   produces identical results without external infrastructure, and
   external implementations can validate their decoder against our
   vectors without a network call.

2. Real-world LEI conformance vectors (anchor-0c-lei):
   - docs/tests/conformance/anchor-0c-lei/lei-public-examples.json
     carries 4 LEIs verified against ISO 17442 mod-97: the GLEIF
     documentation example plus three real public LEIs from the
     GLEIF Global LEI Index (Apple, Microsoft, ECB) chosen to
     exercise alphabetic-prefix LOUs and varied body shapes.
   - Lowercase + whitespace canonicalization vectors confirm the
     resolver's input normalization.
   - 8 reject vectors covering empty, too-short, too-long, hyphen,
     embedded-space, special-character, and two perturbed-check-
     digit cases.
   - lei/conformance_test.go consumes the JSON fixture and runs
     three table-driven test functions. The fixture file is
     language-agnostic; an external Rust or Python implementation
     can consume the same JSON to validate its conformance against
     the same data the Go implementation runs against.

Coverage at 90.2% (held). All RA tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan G test improvement 3 (multi-anchor live scenario) caught a
real bug: registering the same operational FQDN under three
different anchor profiles (FQDN, DID, LEI) failed at the second
registration with BASE_ONLY_FQDN_TAKEN. The pre-fix uniqueness
check fired on agent_host alone, contradicting the proposal's
ANS-0 §7 cross-anchor coexistence rule.

Fix: widen the predicate from agent_host alone to (agent_host,
anchor_type). Each (host, anchor_type) tuple gets its own
uniqueness slot. The legacy "anchor unspecified" path maps to
empty anchor_type and occupies its own implicit slot, distinct
from any explicit anchor profile.

Storage:
- ExistsActiveBaseOnlyByAgentHost gains an anchorType parameter.
- Empty anchorType matches rows with anchor_type IS NULL or empty
  (legacy implicit-FQDN slot).
- Non-empty anchorType matches rows where anchor_type equals the
  argument exactly.

Service:
- checkRegistrationUniqueness reads req.AnchorClaim.AnchorType to
  scope the query. checkBaseOnlyUniqueness was extracted as a
  separate helper to keep the nestif-friendly shape.
- The conflict error now identifies the scope ("anchor=did") so an
  operator hitting the conflict knows which uniqueness slot
  collided.

Tests:
- TestAgentStore_ExistsActiveBaseOnly_AnchorScoped pins the new
  rule: same host different anchor types coexist; same (host,
  anchor) conflicts; legacy implicit-FQDN slot is independent of
  any explicit anchor slot.
- The middleware test fake updated to match the new signature.

Live verification: /tmp/plan-g-multi-anchor.sh registers FQDN +
DID + LEI for multi.plang.example.com against the demo RA, lists
back three rows sharing the same agentHost with three distinct
anchorResolvedId values. Cross-anchor consistency check confirms
the (3 anchor types, 1 host, 3 resolved IDs) shape.

This is also a cleaner expression of ANS's operational semantics:
the agent's operational endpoint (FQDN where it terminates TLS)
can carry multiple identity claims simultaneously, and the
registration store records each claim independently. The
EquivalenceLink event from the proposal §7 will eventually link
these registrations explicitly; until it lands, a verifier
constructs the equivalence graph by scoping the V2 list to a
single agentHost.

Coverage holds at 90.1%. All 222 internal tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan G test improvement 4: did:pkh anchor profile per the W3C CCG
did:pkh method spec and CAIP-10 account identifiers. The path
toward ERC-8004 on-chain agent identity per anchor-0b-did.md §6.

The resolver's offline layer is fully implemented; the on-chain
lookup is delegated to a ChainResolver interface mirroring the LEI
resolver's GLEIFClient pattern. The split keeps the package useful
for testbeds (CAIP-10 validation works without an Ethereum RPC
endpoint) and switches to live resolution by configuration when an
ERC-8004 testnet wiring lands in CI.

Lexical layer (slice-internal):
- did:pkh:<namespace>:<reference>:<address> parsing into a typed
  CAIPAccount (Namespace, Reference, Address).
- Namespace is lowercased per CAIP-2.
- Per-namespace address validation. eip155 enforces the
  "0x" + 40 hex characters shape (case-insensitive); other
  namespaces pass through to the chain resolver implementation
  which owns those rules.
- canonicalizeDIDPkh emits the canonical lowercase URI for the
  IdentityClaim's ResolvedID.

Chain layer (interface only):
- ChainResolver.LookupKey(ctx, CAIPAccount) -> JWK bytes. A
  production implementation will wrap an Ethereum JSON-RPC client
  reading the ERC-8004 IdentityRegistry contract; that lands once
  testnet plumbing is in place.
- Without a ChainResolver injected, Resolve returns
  DID_PKH_CHAIN_NOT_CONFIGURED after lexical validation passes.
  The error code is the migration boundary an operator hits if
  they configure an RA to accept did:pkh without wiring a chain
  client.

Tests cover:
- CAIP-10 parsing across Sepolia, Ethereum mainnet, Anvil chain
  31337, uppercase namespace.
- DID_PKH_BAD_FORMAT for wrong prefix, missing prefix, too few
  parts, empty namespace/reference/address.
- validateEIP155Address against good (canonical, lowercase,
  uppercase, 0X prefix) and bad (missing prefix, wrong length,
  non-hex) inputs.
- DID_PKH_CHAIN_NOT_CONFIGURED when no ChainResolver injected.
- Bad-address propagates DID_PKH_BAD_ADDRESS even when a
  ChainResolver is present (lexical layer fires first).
- Non-eip155 namespace (Solana-style) passes lexical validation
  through to the chain layer (the namespace is unknown to this
  resolver but the parsed shape is admissible).
- Happy path with an injected fake ChainResolver: claim shape,
  IssuedAt/ExpiresAt budget (1h), JWK pass-through.
- Chain-lookup error propagates as DID_PKH_CHAIN_LOOKUP_FAILED.
- Nil JWK from chain resolver propagates as DID_PKH_NO_KEY.
- SupportedProfiles returns ["0.B-did:pkh"].
- CAIPAccount.String round-trip for the canonical wire form.

The Anvil pre-funded address (0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266)
is used as the canonical test address throughout, matching the
foundry/hardhat documentation conventions called out in Grok's
testing guidance.

Coverage holds at 90.2%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scourtney-godaddy scourtney-godaddy added the AI assisted Pull request created with AI assistance label May 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI assisted Pull request created with AI assistance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant