NUT-12: Offline ecash signature validation
optional
In this document, we present an extension of Cashu’s crypto
system to allow a user Alice to verify the mint
Bob’s signature using only Bob’s public
keys. We explain how another user Carol who receives
ecash from Alice can execute the DLEQ proof as well.
This is achieved using a Discrete Log Equality (DLEQ) proof.
Previously, Bob’s signature could only be checked by
himself using his own private keys (NUT-00).
The DLEQ proof
The purpose of this DLEQ is to prove that the mint has used the
same private key a for creating its public key
A (NUT-01) and for signing
the BlindedMessage B'. Bob returns the
DLEQ proof in addition to the blind signature C' for
a mint or swap operation.
The complete DLEQ proof reads
# DLEQ Proof
(These steps occur when Bob returns C')
Bob:
r = HMAC-SHA256(key=a, data="Cashu_DLEQ_R_v1" || A || B' || C' || ctr) # Deterministic nonce
R1 = r*G
R2 = r*B'
e = hash(R1,R2,A,C')
s = (r + e*a) mod n
return e, s
Alice:
R1 = s*G - e*A
R2 = s*B' - e*C'
e == hash(R1,R2,A,C')
If true, a in A = a*G must be equal to a in C' = a*B'
where:
G, n = secp256k1 generator point, and curve order
a = mint's amount-i private key, as 32-byte big-endian (HMAC key)
A, B', C' = uncompressed (65 byte) SEC1 public points as raw bytes
ctr = nonce counter byte; set to 0x00 initially, increment and retry if r == 0 or r >= n (max 256 tries)
[!CAUTION]
Mints SHOULD use a rejection-sampled deterministic nonce (
r) to avoid RNG failures. See the test vectors.Reusing a nonce across different challenges leaks the private key immediately (
a = (s₁ - s₂) · (e₁ - e₂)⁻¹ mod n). Mints that use random nonces MUST source them from a cryptographically secure RNG.
Hash function
The hash function
hash(x: Array[PublicKey]) -> bytes generates a
deterministic SHA256 hash for a given input list of
PublicKey. The uncompressed (32+32+1)-byte
hexadecimal representations (130 characters) of each
PublicKey are concatenated before taking the SHA256
hash.
def hash_e(*publickeys: PublicKey) -> bytes:
e_ = ""
for p in publickeys:
_p = p.serialize(compressed=False).hex()
e_ += str(_p)
e = hashlib.sha256(e_.encode("utf-8")).digest()
return e
[!NOTE] For examples of valid DLEQ proofs, see the test vectors.
Mint to user: DLEQ
in BlindSignature
The mint produces these DLEQ proofs when returning
BlindSignatures in the responses for minting (NUT-04) and swapping (NUT-03) tokens. The
BlindSignature object is extended in the following
way to include the DLEQ proof:
{
"id": <str>,
"amount": <int>,
"C_": <str>,
"dleq": { <-- New: DLEQ proof
"e": <str>,
"s": <str>
}
}
e and s are the DLEQ proof.
User to user: DLEQ in
Proof
In order for Alice to communicate the DLEQ to
another user Carol, we extend the Proof
(see NUT-00) object and include the DLEQ
proof. As explained below, Alice
MUST also include the blinding factor
r for the proof to be convincing to
Carol.
{
"id": <str>,
"amount": <int>,
"secret": <str>,
"C": <str>,
"dleq": { <-- New: DLEQ proof
"e": <str>,
"s": <str>,
"r": <str>
}
}
e and s are the challenge and
response of the DLEQ proof returned by Bob,
r is the blinding factor of Alice that
was used to generate the Proof. Alice
serializes these proofs like any other in a token (see NUT-00) to send it to another user
Carol.
[!IMPORTANT]
Privacy: The entire
dleqfield SHOULD be removed before sending aProofto the mint as an input. The mint can associate theBlindSignaturewith theProofgiveneands, or using the blinding factorr.
Alice (minting user) verifies DLEQ proof
When minting or swapping tokens, Alice receives
DLEQ proofs in the BlindSignature response from the
mint Bob. Alice checks the validity of
the DLEQ proofs for each ecash token she receives via the
equations:
R1 = s*G - e*A
R2 = s*B' - e*C'
e == hash(R1,R2,A,C') # must be True
Here, the variables are
A– the public keyBobused to sign this Proof(e, s)– the DLEQ proof returned byBobB'–Alice’sBlindedMessageC'–Bob’sBlindSignatureonB'
In order to execute the proof, Alice needs
e, s that are returned in the
BlindSignature by Bob.
Alice further needs B' (the
BlindedMessage Alice created and
Bob signed) and C' (the blind signature
in the BlindSignature response) from
Bob, and A (the public key of
Bob with which he signed the BlindedMessage). All
these values are available to Alice during or after
calling the mint and swap operations.
If a DLEQ proof is included in the mint’s
BlindSignature response, wallets
MUST verify the DLEQ proof.
Carol (another user) verifies DLEQ proof
Carol is a user that receives Proofs
in a token from another user Alice. When Alice sends
Proofs with DLEQ proofs to Carol or when
Alice posts the Proofs publicly,
Carol can validate the DLEQ proof herself and verify
Bob’s signature without having to talk to
Bob. Alice includes the following
information in the Proof (see above):
(x, C)– the ecashProof(e, s)– the DLEQ proof revealed byAlicer–Alice’s blinding factor
Here, x is the Proof’s secret, and C
is the mint’s signature on it. To execute the DLEQ proof like
Alice did above, Carol needs
(B', C') which she can compute herself using the
blinding factor r that she receives from
Alice.
To verify the DLEQ proof of a received token,
Carol needs to reconstruct B' and
C' using the blinding factor r that
Alice has included in the Proof she sent
to Carol. Since Carol now has all the
necessary information, she can execute the same equations to
verify the DLEQ proof as Alice did:
Y = hash_to_curve(x)
C' = C + r*A
B' = Y + r*G
R1 = ... (same as Alice)
If a DLEQ proof is included in a received token, wallets MUST verify the proof.
Mint info setting
The NUT-06 nuts object
indicates support for this feature:
{
"12": {
"supported": true
}
}