feat: resolve a DID's current XCH payment address from chain (resolve_xch_address) - #3
Conversation
WIP anchor for #1239: resolve a DID's current XCH payment address from chain via the ChainSource seam (reads only, fail-closed). Implementation lands in follow-up commits on this branch. Refs #1239 #1221 Co-Authored-By: Claude <noreply@anthropic.com>
Adopt the canonical dig-chainsource-interface v0.1.0 ChainSource trait (drop the local trait + SingletonLineage; re-export both to preserve the public paths). Add the money-critical resolve_xch_address / _from_did_string primitives: walk the DID to its tip, authenticate the GENUINE launcher via the parent-spend walk (never the attacker-chosen curried launcher id), then return the owner's bech32m XCH Address. New additive DidError::LauncherMismatch. Co-Authored-By: Claude <noreply@anthropic.com>
…tion SPEC §3 U10 Resolve-XCH-address row, §6 LauncherMismatch row, §10 rewritten for the canonical dig-chainsource-interface trait. README surface + types + module map. CHANGELOG Unreleased. Fix a private intra-doc link in the public rustdoc. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
PASS — correctness/quality gate (independent fresh-context review)
Money-critical resolve_xch_address primitive reviewed against design #1239 (comment 5016826175). Verdict: PASS.
Money-guard load-bearing: CONFIRMED. Neutralized the launcher check (if false && authenticated.launcher_id != launcher_id) — resolve_rejects_an_echoed_different_dids_tip fails (resolves Ok(Some(...)) for the victim launcher instead of LauncherMismatch). The guard at src/resolve.rs:335-338 runs authenticate_singleton (parent-spend walk to the genuine launcher) AFTER walk_did_lineage_to_tip and BEFORE building/returning the address — correctly ordered, genuinely load-bearing, non-tautological.
Fail-closed: CONFIRMED. No path returns a wrong address or spurious Ok(None). Tip reconstructed from parent's genuine singleton successor (coin-id verified, resolve.rs:268-270); address validated via .encode() before return; every wrong case maps to a typed Err (LauncherMismatch/NotDid/NotASingleton/NoIdentitySingleton/LineageTooDeep/Chain/Parse). Spoofed-curry resolving to NotDid (vs NotASingleton) is fail-closed — design-table doc nit only.
Interface adoption, no regression: CONFIRMED. Local ChainSource/SingletonLineage dropped, re-pointed to dig-chainsource-interface = "0.1" (crates.io, no git dep, no [patch.crates-io]); dig_did::ChainSource/SingletonLineage paths preserved via re-export (resolve.rs:47, lib.rs:78-80). Lineage/auth walk compiles + all lineage tests green.
Gates: cargo test 38/38 green (36 lib + 2 golden); clippy -D warnings clean; fmt clean; coverage 91.67% lines / 89.01% regions (≥80%). Docs: SPEC §3 U10 + §6 LauncherMismatch, README, CHANGELOG, lib.rs re-exports (Address + resolve fns) all present. Version 0.4.0, additive. No cruft.
No blocking findings. No pre-existing review threads. Orchestrator: clear to merge on green checks + zero unresolved threads.
What & why (Closes DIG-Network/dig_ecosystem#1239, adopts #1240)
Two coupled changes in one unit of work (§1.3b):
resolve_xch_address— the money-critical DID→XCH payment-address primitive. Walks the DIDsingleton to its current authenticated tip and returns the tip owner's payment
Address(
tip.info.p2_puzzle_hash, bech32m under a caller-suppliedprefix— network-agnostic, nohardcoded HRP). Chain-READ only: never signs, never broadcasts (INV-1/INV-2).
resolve_xch_address(launcher_id, prefix, source) -> DidResult<Option<Address>>resolve_xch_address_from_did_string(did, prefix, source) -> DidResult<Option<Address>>ChainSourceseam onto the canonicaldig-chainsource-interfacev0.1.0 (crates.io):dropped dig-did's LOCAL
ChainSourcetrait +SingletonLineage, depend on the published interface,re-export both so
dig_did::ChainSource/dig_did::SingletonLineageare unchanged (additive).New public surface:
resolve_xch_address,resolve_xch_address_from_did_string,DidError::LauncherMismatch(additive), and a re-exported
chia_sdk_utils::Address.Money-critical soundness (NC-9) — the parent-walk guard
A wrong resolution silently pays the wrong recipient, so the resolved address MUST be provably bound to
the REQUESTED identity. The cheap
tip.info.launcher_id() == launcher_idcheck is INSUFFICIENT — thatcurried launcher id is attacker-chosen (the
pay_to_coin_wearing_a_singleton_puzzle_hashclass). Afterwalk_did_lineage_to_tip, resolve runsauthenticate_singleton(tip.coin.coin_id(), source)— whichwalks the parent-spend chain to the GENUINE launcher — and requires that authenticated launcher to
equal the requested
launcher_id, elseDidError::LauncherMismatch. NC-9 (on-chain data requireson-chain proof, fail-closed) is satisfied by this parent-walk authentication, not by trusting any
attacker-chosen field. Proven load-bearing: neutralizing the guard makes the money test fail (resolves
to the attacker's address).
NC-8 is N/A —
resolve_xch_addressis a chain READ that writes nothing on-chain (no spend, nobroadcast).
Fail-closed None/error table (implemented exactly)
unlaunched/melted →
Ok(None); malformed did:chia →InvalidDidString; tip creating-spend absent →NoIdentitySingleton; tip not a DID →NotDid; spoofed curry → fail-closed (NotDid/NotASingleton);genuine launcher ≠ requested →
LauncherMismatch; chain read fails →Chain; over-deep →LineageTooDeep;Address::encodefails →Parse.Tests (TDD, adversarial)
38 tests pass. The 6 mandated adversarial tests:
resolve_rejects_an_echoed_different_dids_tip(THEmoney test — dishonest source echoes an attacker DID's tip for the victim launcher →
LauncherMismatch,and asserts the result is NOT the attacker's address),
resolve_rejects_a_spoofed_curry_singleton,resolve_returns_none_for_unlaunched_or_melted,resolve_from_did_string_rejects_malformed,resolve_happy_path_matches_the_owner_address,resolved_address_roundtrips_through_decode(+ adid-string happy-path). Uses the interface's
MockChainSource(testing feature) + a Simulator-backedsource for genuine singleton spends. No live network.
Blast radius checked
ChainSourcetrait callers are all in-crate:authenticate_singleton[_bounded],walk_did_lineage_to_tip,prove_lineage, and the test sources. The migration re-exports the interfacetrait/type so the
dig_did::*paths are preserved (additive externally — new crate, no externalimplementors yet).
resolve_xch_addressis net-new (no callers). Risk: LOW-MEDIUM (the trait widensrequired methods, inherent to adopting the canonical interface per #1240; existing lineage tests updated
to implement the full trait).
Quality gates (local)
cargo fmt --all -- --checkclean ·cargo clippy --all-targets --all-features -- -D warningscleancargo llvm-cov nextest --all --fail-under-lines 80→ 91.67% lines / 89.01% regionsRUSTDOCFLAGS=-D warnings cargo doc --no-depsclean · version bumped 0.3.0 → 0.4.0 (minor: additive)Do NOT merge — handing back to the orchestrator for the triple gate.