NUT-12: domain-tagged raw-byte challenge hash (hash_e v2) #369

Open
opened 2026-05-04 12:28:34 +00:00 by robwoodgate · 0 comments
robwoodgate commented 2026-05-04 12:28:34 +00:00 (Migrated from github.com)

The current challenge hash concatenates the hex-encoded text of each point before hashing:

e = SHA256(hex(R1) || hex(R2) || hex(A) || hex(C'))

Each uncompressed point is 65 bytes -> 130 lowercase hex characters, encoded as UTF-8 bytes before the SHA256. The lengths are fixed so there's no boundary ambiguity, but there's an encoding pipeline that cross-language implementations have to get right in full.

We could use a cleaner form, following BIP-340 conventions:

e = SHA256(b"Cashu_DLEQ_E_v1" || R1 || R2 || A || C')

Notes

  • Simpler implementation: Points are raw uncompressed SEC1 bytes (65 bytes each). The domain tag is 15 UTF-8 bytes. No intermediate encodings required.

  • This doesn't close any known attack: the current encoding is unambiguous. The motivation is implementation correctness: wrong hex case, wrong text encoding, or a missing encode step can all silently produce a different hash.

  • Migration: this is a breaking change. All existing DLEQ proofs would fail under the new hash scheme.

  • Implementations would need to try the new hash first and fall back to the legacy function during a transition window while tokens cycle out of circulation.

  • Suggest this follows #368 once that settles: the nonce change was free, this one needs a coordinated migration

The current challenge hash concatenates the hex-encoded text of each point before hashing: ``` e = SHA256(hex(R1) || hex(R2) || hex(A) || hex(C')) ``` Each uncompressed point is 65 bytes -> 130 lowercase hex characters, encoded as UTF-8 bytes before the SHA256. The lengths are fixed so there's no boundary ambiguity, but there's an encoding pipeline that cross-language implementations have to get right in full. We could use a cleaner form, following BIP-340 conventions: ``` e = SHA256(b"Cashu_DLEQ_E_v1" || R1 || R2 || A || C') ``` ## Notes - Simpler implementation: Points are raw uncompressed SEC1 bytes (65 bytes each). The domain tag is 15 UTF-8 bytes. No intermediate encodings required. - This doesn't close any known attack: the current encoding is unambiguous. The motivation is _implementation correctness_: wrong hex case, wrong text encoding, or a missing encode step can all silently produce a different hash. - Migration: this is a breaking change. All existing DLEQ proofs would fail under the new hash scheme. - Implementations would need to try the new hash first and fall back to the legacy function during a transition window while tokens cycle out of circulation. - Suggest this follows #368 once that settles: the nonce change was free, this one needs a coordinated migration
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo-admin/nuts#369
No description provided.