fix(ecash): sign with the mint's SAT keyset, not whichever came first

`get_active_sat_keyset` picked the first keyset with a non-empty key map,
and `MintKeyset` had no `unit` field to filter on — so on a multi-unit mint
the wallet signed sat-denominated mint/swap requests against a usd or eur
keyset. The mint refuses that with `11013 Unit unsupported`, which is
exactly what claiming minted coins hit against testnut.cashu.space (it
serves usd, eur, msat and sat keysets). Minibits is sat-only, so this
latent bug never surfaced in production — the test-mint switch found it on
its first run.

MintKeyset now carries `unit` and `active`, both defaulted so a sat-only
mint that omits them still parses, and selection filters to sat and prefers
an active keyset.

Also: pin BIP-39 seed derivation to the specification's own test vectors.
The node's entire identity hangs off `Mnemonic::to_seed("")`, and the
`bip39` crate is no longer version-pinned (the exact pin had to be relaxed
so `cashu` could resolve). A bump that changed derivation would silently
re-key every node on the fleet and orphan every backup; both vectors —
empty passphrase and the NFKD-exercising passphrase arm — now fail the
suite instead. Verified byte-identical under the newly resolved 2.2.2.

And the route script polls the mint's quote state before claiming: the test
mint settles its own invoices, but not instantly, so claiming immediately
raced the settlement and reported a spurious "Quote not paid".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-17 06:07:27 -04:00
co-authored by Claude Fable 5
parent be2cfb8293
commit 26638aa621
4 changed files with 83 additions and 6 deletions
+39
View File
@@ -1033,4 +1033,43 @@ mod tests {
"release-root public key KAT"
);
}
/// The node's whole identity hangs off `Mnemonic::to_seed("")`, so this
/// pins that derivation to the BIP-39 specification vectors rather than to
/// whatever the `bip39` crate happens to do today.
///
/// It exists because the crate is not version-pinned any more: the exact
/// `=2.1.0` pin was relaxed to `"2.1"` in 2026-08 so the `cashu` crate
/// could resolve (the pin transitively froze `unicode-normalization` at a
/// version with no common solution). A bump that changed derivation would
/// silently re-key every node on the fleet and orphan every existing
/// backup, which no amount of code review reliably catches — this does.
#[test]
fn seed_derivation_matches_the_bip39_specification_vectors() {
let words = "abandon abandon abandon abandon abandon abandon abandon \
abandon abandon abandon abandon about"
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
let mnemonic: bip39::Mnemonic = words.parse().expect("valid test mnemonic");
// Empty passphrase — exactly how MasterSeed::from_mnemonic derives.
assert_eq!(
hex::encode(mnemonic.to_seed("")),
"5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc1\
9a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
.replace(['\n', ' '], ""),
"BIP-39 seed derivation changed — every node's keys would move"
);
// With a passphrase, where NFKD normalisation actually participates;
// this is the arm a `unicode-normalization` change could disturb.
assert_eq!(
hex::encode(mnemonic.to_seed("TREZOR")),
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553\
1f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"
.replace(['\n', ' '], ""),
"BIP-39 passphrase normalisation changed"
);
}
}