diff --git a/tests/test_nip44_v2.py b/tests/test_nip44_v2.py index 247c0ac..f3b27b9 100644 --- a/tests/test_nip44_v2.py +++ b/tests/test_nip44_v2.py @@ -242,31 +242,130 @@ class TestPaddingFormula: # ============================================================================= -@pytest.mark.skip( - reason=( - "Waiting on bitspire to post one sample encrypted event to " - "~/dev/coordination/log.md per the 2026-05-30T15:55Z entry. Once " - "posted, hardcode the (event_id, content, recipient_privkey, " - "expected_plaintext) fixture here and remove the skip — this test " - "is the byte-compat cross-test between our hand-rolled NIP-44 v2 " - "and the nostr-tools impl the ATM uses." - ) -) -def test_decrypts_bitspire_sample_event_from_coord_log(): - """Cross-impl byte-compatibility test. Bitspire generates one event on - their side (nostr-tools NIP-44 v2 impl), posts the raw event JSON + - a known throwaway recipient privkey to the coord log, and we assert - our `decrypt_from` recovers the expected `{"denominations": {...}}` - plaintext. +# ----------------------------------------------------------------------------- +# Bitspire-side fixture, posted to ~/dev/coordination/log.md at 2026-05-30T13:15Z. +# Throwaway keypairs (one-shot, never sign anything else) — safe to embed verbatim. +# Generated by apps/machine/src/services/operator-config.ts-shape code path using +# the @bitSpire/nostr-client encryptContentV2 + createSignedEvent helpers (same +# code the production bootstrap publish uses). Round-tripped on bitspire side +# before posting. +# ----------------------------------------------------------------------------- - If this passes, both impls produce byte-identical wire format. If it - fails, the spec ambiguity surfaces before either side ships — exactly - what bitspire flagged in the plan review (`07:55Z`). - """ - # event_b64_content = "..." # paste from coord log - # sender_pubkey_hex = "..." - # recipient_privkey_hex = "..." - # expected_plaintext = '{"denominations": {"20": {"position": 1, "count": 49}}}' - # recovered = decrypt_from(event_b64_content, recipient_privkey_hex, sender_pubkey_hex) - # assert recovered == expected_plaintext - raise NotImplementedError("fixture pending — see skip reason") +_BITSPIRE_FIXTURE = { + "atm_keypair": { + "privkey_hex": ( + "a1601b05967cb421056f197008eca1dfa61f0eb5b505c277a0d4ca6b053e91f2" + ), + "pubkey_hex": ( + "8db588b6431edbbc0c4f7517bc90447cec34c866b7110e63c88e20a4cccd0e5c" + ), + }, + "operator_keypair": { + "privkey_hex": ( + "216030bdda5aa47c37b74117bc29612bfc18d8122f70e80cb7a6d875c8699108" + ), + "pubkey_hex": ( + "052f27837c3c46b5086825805b8d061ed64346e61cd0c3013725e544aa2a0b49" + ), + }, + "expected_plaintext": { + "denominations": { + "20": {"position": 1, "count": 49}, + "50": {"position": 2, "count": 100}, + }, + }, + "event": { + "kind": 30078, + "content": ( + "AgUSQOlYyF7JomOKqJSyAOF/O7yR1d2DYgXvXUS7sBMqRbKPM+ACmkT/R6owFd22nRf2" + "k+KEibEi+WcK6+acBwy1ThWP2NHUlrMp8qjUYrV1XXJXwRLOlLBe0LHmioFi6jTyJxSE" + "/Z+z79o7wki60CKDoNZqSRiScRN0lT7tzEgsFXo2vFzPdzEQwy/jk154DgBoCiRIRjtX" + "kBNGGlN9ABPPfw==" + ), + "tags": [ + [ + "d", + "bitspire-cassettes-state:" + "8db588b6431edbbc0c4f7517bc90447cec34c866b7110e63c88e20a4cccd0e5c", + ], + [ + "p", + "052f27837c3c46b5086825805b8d061ed64346e61cd0c3013725e544aa2a0b49", + ], + ], + "created_at": 1780156459, + "pubkey": ( + "8db588b6431edbbc0c4f7517bc90447cec34c866b7110e63c88e20a4cccd0e5c" + ), + "id": ( + "28e2bd428bca5b522c037d06e962f5c2ed2e40c398f7ecf84ed5f6272ab77ae4" + ), + "sig": ( + "8bbde91fb39cfe7026384ca89843b3f9aaf5b9a9a90ddc20e09bc056721438b2" + "9d032435e71bb16a5ac211c951de02d8e2f5422d9ee110653f6e3df72238f6dd" + ), + }, +} + + +class TestBitspireCrossTest: + """Byte-compat cross-test between our hand-rolled NIP-44 v2 (`nip44.py`) + and the nostr-tools NIP-44 v2 impl that bitspire uses on the ATM side + (via @bitSpire/nostr-client). If these tests pass, the wire format + agrees across both implementations and the joint round-trip (operator + publish → ATM apply / ATM bootstrap → operator consume) is byte-safe. + If any fail, the spec ambiguity surfaces before sintra ships.""" + + def test_decrypts_bitspire_sample_event(self): + """The load-bearing assertion: our `decrypt_from` recovers the + expected `{"denominations": {...}}` plaintext from bitspire's + encrypted event content.""" + import json + + event = _BITSPIRE_FIXTURE["event"] + operator_privkey = _BITSPIRE_FIXTURE["operator_keypair"]["privkey_hex"] + + from ..nip44 import decrypt_from + + plaintext = decrypt_from( + event["content"], + operator_privkey, + event["pubkey"], + ) + assert json.loads(plaintext) == _BITSPIRE_FIXTURE["expected_plaintext"] + + def test_signature_verifies_via_lnbits_helper(self): + """Optional extra per bitspire's 13:15Z note (3). The consumer + path runs verify_event before NIP-44 decrypt — locking the sig- + algorithm agreement here means both sides hash the event id the + same way + Schnorr-verify under the same x-only public-key + convention.""" + from lnbits.utils.nostr import verify_event + + assert verify_event(_BITSPIRE_FIXTURE["event"]) is True + + def test_encrypt_round_trip_via_our_impl_decrypts_with_their_keys(self): + """Optional extra per bitspire's 13:15Z note (3). Encrypt the + expected plaintext using OUR impl with the ATM keypair as + sender + operator pubkey as recipient. The resulting ciphertext + won't be byte-identical to the fixture (NIP-44 v2 nonces are + random) but it MUST decrypt back to the same plaintext when + passed to our decrypt path. Locks the encrypt direction too, + not just decrypt.""" + import json + + from ..nip44 import decrypt_from, encrypt_for + + plaintext = json.dumps( + _BITSPIRE_FIXTURE["expected_plaintext"], separators=(",", ":") + ) + atm_sec = _BITSPIRE_FIXTURE["atm_keypair"]["privkey_hex"] + atm_pub = _BITSPIRE_FIXTURE["atm_keypair"]["pubkey_hex"] + op_sec = _BITSPIRE_FIXTURE["operator_keypair"]["privkey_hex"] + op_pub = _BITSPIRE_FIXTURE["operator_keypair"]["pubkey_hex"] + + our_ciphertext = encrypt_for(plaintext, atm_sec, op_pub) + recovered = decrypt_from(our_ciphertext, op_sec, atm_pub) + assert json.loads(recovered) == _BITSPIRE_FIXTURE["expected_plaintext"] + # The two ciphertexts SHOULD differ (random nonce per encrypt) + assert our_ciphertext != _BITSPIRE_FIXTURE["event"]["content"]