[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
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
did:keyresolver underinternal/adapter/anchor/did/: pureoffline DID method. Multibase prefix validation (base58btc
zonly), pure-Go base58btc decoder usingmath/big(preservesleading 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 4short path) so the JWK carriesboth x and y coordinates. X25519 (0xEC) and P-256 (0x1200)
surface
DID_KEY_TYPE_NOT_IMPLEMENTEDto flag the migrationboundary.
Tests cover two W3C-cited Ed25519 vectors with x values
verified by round-tripping through this resolver, all five
DID_BAD_FORMATandDID_KEY_*reject paths, multicodecvarint single + two-byte cases plus truncation, leading-zero
base58btc round-trip, length validation, secp256k1 happy
path through the curve generator G.
did:pkhresolver: CAIP-10 chain identifier parsing(
did:pkh:<namespace>:<reference>:<address>), eip155 addressvalidation (
0x+ 40 hex),ChainResolverinterface stub foron-chain lookup. The interface mirrors the LEI resolver's
GLEIFClientinjection pattern: lexical validation workswithout a chain client; a production HTTP-RPC client lands when
ERC-8004 testnet plumbing is in place. The Anvil pre-funded
address (
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) is thecanonical test address throughout, matching foundry/hardhat
documentation conventions.
Tests cover Sepolia, Ethereum mainnet, Anvil chain 31337,
uppercase namespace lowercased, all
DID_PKH_BAD_FORMATcases,validateEIP155Addressagainst good and bad inputs,DID_PKH_CHAIN_NOT_CONFIGUREDboundary, fake-chain happy path,chain-lookup errors, empty-JWK rejection.
Real public LEI conformance vectors at
docs/tests/conformance/anchor-0c-lei/lei-public-examples.jsoncarry 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:
Base-only uniqueness scope widened from
agent_hostaloneto
(agent_host, anchor_type). The pre-fix predicate causeda real bug: registering
multi.example.comfirst under theFQDN anchor and then under DID rejected the second
registration with
BASE_ONLY_FQDN_TAKEN, contradicting thespec'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_AnchorScopedpins thenew 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)TestAgentStore_ExistsActiveBaseOnly_AnchorScoped)🤖 Generated with Claude Code