Skip to content

v0.5.0: e2e faucet verify, local network, aggregator critical checks#3

Merged
vrogojin merged 5 commits into
mainfrom
feat/local-network-and-standalone-status
May 24, 2026
Merged

v0.5.0: e2e faucet verify, local network, aggregator critical checks#3
vrogojin merged 5 commits into
mainfrom
feat/local-network-and-standalone-status

Conversation

@vrogojin

Copy link
Copy Markdown
Contributor

Summary

  • Faucet probe rewritten as end-to-end mint-and-verify (breaking). Spins up an ephemeral Sphere wallet, mints a single-use nametag on the L3 aggregator, publishes the kind:30078 binding, requests 1 raw unit of USDU from the faucet, and waits up to 10s for the kind:31113 token-transfer event to arrive — then asserts the delivered token's coinId + amount match the faucet's declared amountInSmallestUnits. The previous bogus-nametag check correctly exercised the HTTP/parse/resolve pipeline but couldn't catch the failure mode that actually breaks downstream e2e suites: the faucet accepts a real mint request, returns success:true, and yet no token ever lands at the recipient.
  • local network added for the sphere-sdk docker-compose stack (E2E_FULL_LOCAL_STACK=1). Aggregator/Nostr/IPFS point at loopback; Fulcrum + Market keep public testnet URLs so a single command can surface "your local stack is fine; one of its public dependencies is down."
  • Aggregator verdict rule tightened. submit_commitment and get_inclusion_proof now carry critical:true; any single critical fail forces unreachable (CLI exit 2), not degraded (exit 1). Catches the sphere-sdk #191 incident class where a broken write path was silently classified as merely "degraded" and let e2e gates pass. The aggregator's /health parser also now accepts both BFT ({status:"healthy"}) and standalone ({status:"ok", role:"standalone"}) response shapes.
  • CLAUDE.md hard rules scoped down. Three rules — minimal-deps, no-SDK-coupling, stateless-on-relay — now have explicit "with one exception" carve-outs for the faucet probe, plus a "The faucet exception" section recording the rationale, dependency cost (@unicitylabs/sphere-sdk ≈ 74 MB), per-run side effects (one nametag NFT, one kind:30078 event, 1e-6 USDU consumed), and the trigger to revisit (faucet growing a probe-only mode). All other probes still uphold the original rules.

Test plan

  • Network-free smoke tests pass (31/31), including new contract test for the faucet probe's required opts
  • Live testnet probe end-to-end — all 6 services HEALTHY
  • Live --only faucet reports the new three-check shape (wallet-setup / request / receipt) and verifies real USDU delivery in ~9s
  • CLAUDE.md \$\$ref to "The faucet exception"\$\$ link target resolves
  • CI green on push
  • Downstream e2e gates that depend on faucet probe behavior (if any) updated for the new check names

🤖 Generated with Claude Code

vrogojin and others added 5 commits May 4, 2026 17:01
Adds a sixth probe to the suite for the Unicity test faucet
(https://faucet.unicity.network). Many SDK e2e suites depend on the
faucet for wallet funding (uxf-send-receive, pointer-roundtrip,
migrate-to-profile-conservation, profile-export-roundtrip); when the
faucet is down, those suites silently time out at 240 s with a generic
"Faucet top-up timed out" message. Probing upfront converts that into
a clean 1-2 s SKIP with a precise diagnostic.

The probe issues POST /api/v1/faucet/request with a deliberately-
invalid probe nametag (`infra-probe-do-not-mint-zk7q3xa9p2v`).
Healthy faucet returns HTTP 4xx + structured
`{success: false, error: "Nametag not found: ..."}`. This exercises the
full HTTP/parse/nametag-resolve/response-shaping pipeline WITHOUT
consuming actual faucet quota or requiring a real wallet — the same
"empty cost, full coverage" pattern the aggregator probe uses with its
known-bad shard ID.

Defense-in-depth: if the faucet ever returns success:true for the
probe nametag, the probe DOWNGRADES to 'degraded' (validation may be
broken — the probe nametag is supposed to be invalid).

Network config:
- testnet, dev: faucet = "https://faucet.unicity.network"
- mainnet: faucet = null (no faucet by design)
The probe layer treats null as a clean skip with verdict 'healthy' —
cleaner than emitting a misleading "unreachable" verdict against a
default URL that doesn't apply to the network being probed.

SERVICES expands from 5 to 6: ['nostr', 'aggregator', 'ipfs',
'fulcrum', 'market', 'faucet']. The 'faucet' entry is appended at the
end so existing --only invocations are unaffected.

Tests:
- 21 smoke tests pass (added 1 for mainnet/testnet faucet contract)
- Live testnet probe: 6/6 services healthy
- Live mainnet probe with --only faucet: skipped cleanly with
  "network mainnet has no faucet by design — skipped"
…res as unreachable

The verdict logic counted ALL failed checks uniformly — one fail = degraded,
two fails = unreachable. That treated submit_commitment (the canonical
write-path functional check the aggregator exists to serve) as no more
important than a diagnostic JSON-RPC plane probe.

Symptom: testnet returns HTTP 401 Unauthorized on submit_commitment while
/health and get_block_height keep working. Old verdict: `DEGRADED` (exit 1).
Downstream e2e pre-flight gates that only fire on exit code 2 silently let
test suites run against an aggregator that cannot accept commitments,
producing 35 phantom `Submit failed: [object Object]` failures on the
sphere-sdk side (companion issue: sphere-sdk #191).

Fix:
  - Mark submit_commitment + get_inclusion_proof checks with `critical: true`.
  - Extract verdict computation into a pure `computeAggregatorStatus(checks)`
    function, exported so the rule is testable network-free.
  - Rule: any critical-check fail → 'unreachable'. Non-critical fails follow
    the legacy "≥ 2 → unreachable, exactly 1 → degraded" rule. Warns
    continue to drive 'degraded'.
  - 8 new smoke tests pin the rule: healthy / non-critical degraded / warn /
    critical-fail unreachable (both checks independently) / multi-fail legacy
    preserved / critical-fail-wins-over-warn / explicit critical:false.

Encodes the CLAUDE.md "False-negative discipline" principle directly: the
functional layer "is what catches real outages that liveness misses" — and
when it catches them, the verdict MUST reflect that the service is
unusable for its intended purpose, not merely slow.

Bumps to 0.4.1.
Companion work for the sphere-sdk hermetic e2e stack
(tests/e2e/local-infra/) which boots a local aggregator in
BFT_ENABLED=false standalone mode. Two changes layered on top of #1's
critical-check verdict fix:

1. NETWORKS.local — endpoints for the docker-compose stack:
   - aggregator http://127.0.0.1:3001
   - nostr     ws://127.0.0.1:7777
   - ipfs      http://127.0.0.1:8082
   Fulcrum/Market intentionally non-local (no local counterpart yet).
   Faucet null — the local faucet is DM-driven, not HTTP, so the
   existing faucet probe doesn't apply.

2. aggregator /health body parser accepts both shapes:
   - BFT mode:        {"status":"healthy","database":"ok","aggregators":{...}}
   - Standalone mode: {"status":"ok","role":"standalone","details":{"database":"connected",...}}
   Previously the standalone shape was reported as `degraded` even
   when the aggregator was fully functional (submit_commitment +
   get_inclusion_proof both passing).

Verified against the live local stack:
  unicity-infra-probe --network local --only aggregator
  → HEALTHY (4/4 checks passed), exit 0.

Tests: 30/30 pass. Two new tests cover NETWORKS.local shape +
NETWORKS.local.faucet === null.

Bumps to 0.4.2.
…ical checks)

The agent-facing guide had drifted from the code: faucet probe added in
0.4.0, `local` docker-compose network in 0.4.2, and the aggregator
critical-check verdict rule (sphere-sdk #191 follow-up) were all
documented in commits but not in the contributors' single source of
truth. New agents were re-deriving these from git log — which is what
this file exists to prevent. Also adds the required Claude Code header,
a Common commands section, and pins the `faucet: null` clean-skip
pattern so the next "optional service" addition follows precedent
instead of inventing a new convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous faucet probe sent a deliberately-invalid nametag and
treated the faucet's "Nametag not found" rejection as proof-of-life.
That correctly exercised the HTTP/parse/resolve pipeline but couldn't
catch the failure mode that actually matters to downstream e2e suites:
the faucet accepts a real mint request, returns success:true, and yet
no token ever lands at the recipient.

A live testnet run also surfaced a confusing UX consequence — the
"Nametag not found" string next to a green-tick check reads as a
contradiction even when the verdict is correct, leading operators to
distrust the probe.

This commit replaces the rejection-handshake with the full mint round-
trip. The probe now spins up an ephemeral Sphere wallet, mints a
single-use nametag on the L3 aggregator, publishes the kind:30078
binding, requests 1 raw unit (1e-6) of USDU from the faucet, and waits
up to 10s for the corresponding kind:31113 token-transfer event to
arrive. The SDK handles NIP-04 decryption + Token deserialization. We
then compare the delivered token's coinId + amount against the faucet's
own HTTP-response declaration (amountInSmallestUnits) — independent
proof the mint actually landed.

The faucet has no probe-only mode and no direct-pubkey shortcut, so
verifying real delivery requires running as a one-shot wallet. The
trade-off taken to keep the implementation tractable was pulling in
@unicitylabs/sphere-sdk as a dependency, which violates three of the
project's "Hard rules" in CLAUDE.md (minimal-deps, no-SDK-coupling,
stateless-on-relay). All three rules are now explicitly scoped down
with "with one exception" carve-outs and a "The faucet exception"
section that records the rationale and what to revisit if the faucet
ever grows a probe-only mode.

BREAKING CHANGE: the faucet probe's check names changed
(request/health → wallet-setup/request/receipt) and all three are now
critical:true. JSON consumers that filter on the previous check names
will need to update. End-to-end wall-clock is now ~8–12s (up from
<500ms); the orchestration layer auto-bumps the faucet's timeout
ceiling to at least 30s.

The probe now leaves a kind:30078 event on the Nostr relay + a nametag
NFT on the L3 aggregator + consumes 1 USDU raw unit (≈ economically
zero) per run. Documented in CLAUDE.md "Stateless on the
relay/gateway side, with one exception".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant update to the infrastructure probe tool, most notably adding a new end-to-end Faucet probe that performs a real mint-and-verify cycle using the @unicitylabs/sphere-sdk. It also enhances the aggregator probe by implementing a "critical check" logic, ensuring that functional failures (such as submission rejections) correctly drive an unreachable status rather than a merely degraded one. Additionally, a new local network profile has been added to support testing against hermetic docker-compose stacks. Review feedback correctly identified a potential TypeError in the faucet probe when handling token amounts, suggesting a more robust null check before BigInt conversion.

Comment thread src/probes/faucet.mjs
// about what its async send actually delivered — a real bug to
// surface, but the faucet DID deliver something, so degraded
// (not unreachable).
if (expectedRawAmount != null && BigInt(minted.amount) !== BigInt(expectedRawAmount)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The BigInt constructor throws a TypeError if its argument is null or undefined. Adding a check for minted.amount ensures the probe doesn't crash if the SDK returns an unexpected token structure.

Suggested change
if (expectedRawAmount != null && BigInt(minted.amount) !== BigInt(expectedRawAmount)) {
if (expectedRawAmount != null && minted.amount != null && BigInt(minted.amount) !== BigInt(expectedRawAmount)) {

@vrogojin vrogojin merged commit 2c71edb into main May 24, 2026
1 of 2 checks passed
@vrogojin vrogojin deleted the feat/local-network-and-standalone-status branch May 24, 2026 13:59
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