From cbbd20e22e907d28cfd9b2651c533f7dfea137ad Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 17 Aug 2026 08:06:27 -0400 Subject: [PATCH] test(ecash): cover cashuB emission, the backup phrase, and restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four things the suite could not previously catch: - The emitted token is cashuA. It is still valid, so nothing fails — the send succeeds and the receiver redeems it. The only symptom of cashuB encoding falling back is a warning in the journal nobody reads, which is exactly the kind of silent regression a route check exists for. - The wallet has no backup phrase. Without one the coins live in exactly one file and nothing can bring them back. - The phrase changes between reveals, which would orphan every coin minted under the previous one. - Restore double-counts. It runs against a live wallet, so running it twice must leave the balance where it was. Reveal is also asserted to refuse a wrong password: it is the one route here that hands out key material, and a session alone must not be enough. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/test-ecash-routes.sh | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/scripts/test-ecash-routes.sh b/scripts/test-ecash-routes.sh index 7fa64268..16b821ee 100755 --- a/scripts/test-ecash-routes.sh +++ b/scripts/test-ecash-routes.sh @@ -160,6 +160,14 @@ if [ "${BAL:-0}" -ge 4 ] 2>/dev/null; then if [ -n "$TOKEN" ]; then ok "ecash-send produced a token (${#TOKEN} chars, ${TOKEN:0:7}…)" + # cashuB (V4) is what we emit now; cashuA remains valid but is only the + # fallback, so seeing it here means V4 encoding silently failed. + case "$TOKEN" in + cashuB*) ok "token is cashuB (V4)" ;; + cashuA*) bad "token is cashuA — cashuB encoding fell back, check the journal" ;; + *) bad "token has an unknown prefix: ${TOKEN:0:8}" ;; + esac + res="$(rpc wallet.ecash-receive "{\"token\":\"$TOKEN\"}")" got="$(printf '%s' "$res" | jqf result.received_sats)" [ -n "$got" ] && ok "ecash-receive redeemed ${got} sats" \ @@ -176,6 +184,63 @@ else log " (skipping send/receive — balance ${BAL:-0} sats too low)" fi +# ── NUT-13 backup phrase + restore ──────────────────────────────────────── +# The wallet's backup story: without a phrase the coins live in exactly one +# file and nothing can bring them back, so "is it active" is the check that +# matters most here. +res="$(rpc wallet.ecash-seed-status)" +SEED_ACTIVE="$(printf '%s' "$res" | jqf result.active)" +if [ -n "$SEED_ACTIVE" ]; then + ok "ecash-seed-status reports active=$SEED_ACTIVE" +else + bad "ecash-seed-status: $(printf '%s' "$res" | err_of)" +fi + +# Reveal doubles as activation on a node that predates NUT-13 — the password +# prompt is the only moment the encrypted master seed can be opened. It never +# overwrites an established phrase, so this is safe to run repeatedly. +res="$(rpc wallet.ecash-seed-reveal "{\"password\":\"$ARCHY_PASSWORD\"}")" +WORDS="$(printf '%s' "$res" | jqf result.word_count)" +if [ "$WORDS" = "24" ]; then + ok "ecash-seed-reveal returned 24 words (source: $(printf '%s' "$res" | jqf result.source))" +else + bad "ecash-seed-reveal: $(printf '%s' "$res" | err_of)" +fi + +# Reveal must stay gated. A wrong password returning words would make the +# phrase readable by anyone with a session. +res="$(rpc wallet.ecash-seed-reveal '{"password":"definitely-not-the-password"}')" +[ -z "$(printf '%s' "$res" | jqf result.word_count)" ] \ + && ok "seed reveal refuses a wrong password" || bad "SEED REVEALED WITH A WRONG PASSWORD" + +# The phrase must be stable: a second reveal returning different words would +# mean the wallet re-derived a new one and orphaned every coin minted so far. +w1="$(rpc wallet.ecash-seed-reveal "{\"password\":\"$ARCHY_PASSWORD\"}" | jqf result.words)" +w2="$(rpc wallet.ecash-seed-reveal "{\"password\":\"$ARCHY_PASSWORD\"}" | jqf result.words)" +if [ -n "$w1" ] && [ "$w1" = "$w2" ]; then + ok "the backup phrase is stable across reveals" +else + bad "the backup phrase CHANGED between reveals" +fi + +# Restore is additive and idempotent, so it is safe against a live wallet. +# Running it twice must not double the balance — that would mean re-adding +# coins already held. +res="$(rpc wallet.ecash-restore)" +if [ -z "$(printf '%s' "$res" | err_of)" ]; then + ok "ecash-restore scanned $(printf '%s' "$res" | jqf result.keysets_scanned) keyset(s), recovered $(printf '%s' "$res" | jqf result.recovered_sats) sats" + AFTER_FIRST="$(rpc wallet.ecash-balance | jqf result.cashu_sats)" + rpc wallet.ecash-restore >/dev/null + AFTER_SECOND="$(rpc wallet.ecash-balance | jqf result.cashu_sats)" + if [ "$AFTER_FIRST" = "$AFTER_SECOND" ]; then + ok "restore is idempotent (balance steady at ${AFTER_FIRST} sats)" + else + bad "restore is NOT idempotent: ${AFTER_FIRST} -> ${AFTER_SECOND} sats" + fi +else + bad "ecash-restore: $(printf '%s' "$res" | err_of)" +fi + # ── malformed input handling ────────────────────────────────────────────── res="$(rpc wallet.ecash-receive '{"token":"not-a-token"}')" [ -n "$(printf '%s' "$res" | err_of)" ] \