test: DLEQ proof for blinded-signature key equality#95
Conversation
Adds a self-contained test exercising a Cashu/BDHKE-style DLEQ (Discrete Log Equality) proof: Bob proves the same secret a links A = a*G and C-prime = a*B-prime without revealing a. Covers the valid round-trip plus four soundness cases (wrong A, wrong C-prime, tampered s, tampered e). All proof/verify helpers live in the test file; no production code is touched.
There was a problem hiding this comment.
✅ LGTM — Approved
Clean, self-contained test. No production code changed. DLEQ math is correct.
Verified
- Proof/verify algebra:
s*G - e*A = r*Gands*B' - e*C' = r*B'— checked manually, implementation matches. - Test coverage: 5 subtests covering valid proof + 4 independent failure modes (wrong A, wrong C', tampered s, tampered e). Good discrimination.
- No cross-repo impact: test-only file, no API/type/proto changes.
- Existing primitives reused:
btcec/v2point/scalar ops consistent withtweak.goandec_ops.go.
Minor notes (non-blocking)
-
mustPrivKeyFromSeednear-duplicate (dleq_test.go:65):tweak_test.go:96already hasmustPrivKeyFromSeedWithPrefixwhich does the same core thing (SHA-256 seed →PrivKeyFromBytes). Could reuse it with a don't-care prefix, but not worth blocking over. -
SetBytesoverflow ignored (dleq_test.go:82,dleq_test.go:116):ModNScalar.SetBytessilently reduces mod N when the 32-byte hash ≥ curve order. Standard for Schnorr-like challenge hashing and acceptable for test code — just noting for awareness. -
Deterministic nonce (
dleq_test.go:77):r = H(a || B' || C')is fine for reproducible tests. In any future production DLEQ prover, this would need RFC 6979-style derivation or a CSPRNG. The comment already calls this out — good.
There was a problem hiding this comment.
✅ LGTM — Approved
Clean, self-contained test. No production code changed. DLEQ math is correct.
Verified
- Proof/verify algebra:
s*G - e*A = r*Gands*B' - e*C' = r*B'— checked manually, implementation matches. - Test coverage: 5 subtests covering valid proof + 4 independent failure modes (wrong A, wrong C', tampered s, tampered e). Good discrimination.
- No cross-repo impact: test-only file, no API/type/proto changes.
- Existing primitives reused:
btcec/v2point/scalar ops consistent withtweak.goandec_ops.go.
Minor notes (non-blocking)
-
mustPrivKeyFromSeednear-duplicate (dleq_test.go:65):tweak_test.go:96already hasmustPrivKeyFromSeedWithPrefixwhich does the same core thing (SHA-256 seed →PrivKeyFromBytes). Could reuse it with a don't-care prefix, but not worth blocking over. -
SetBytesoverflow ignored (dleq_test.go:82,dleq_test.go:116):ModNScalar.SetBytessilently reduces mod N when the 32-byte hash ≥ curve order. Standard for Schnorr-like challenge hashing and acceptable for test code — just noting for awareness. -
Deterministic nonce (
dleq_test.go:77):r = H(a || B' || C')is fine for reproducible tests. In any future production DLEQ prover, this would need RFC 6979-style derivation or a CSPRNG. The comment already calls this out — good.
Adds an end-to-end test that checks a Cashu/BDHKE-style DLEQ proof entirely in Arkade Script. The public instance (G, A, B', C') is baked into the locking script and the proof (e, s) is supplied as the witness. The script recomputes R1 = s*G - e*A and R2 = s*B' - e*C' with OP_ECMUL/ OP_ECADD (using (n-e)*P for the subtraction), reconstructs the SEC1 compressed points from their affine coordinates, derives the Fiat-Shamir challenge with OP_SHA256/OP_BIN2NUM/OP_MOD, and asserts it matches e. Covers the valid proof plus tampered-s, tampered-e, and a fully self-consistent proof for a different secret (rejected by the in-script hash binding to A and C').
There was a problem hiding this comment.
Re-review — new commit 0a13dca2 (DLEQ script test)
Traced the full script construction in dleq_script_test.go opcode-by-opcode. Verified:
Stack depth tracking — The builder's depth variable and the pick helper correctly compute OP_PICK offsets for the three base items (e=0, s=1, n-e=2) throughout the entire script. Every scalarMul, ecAdd, serializeCompressed, alt-stack shuttle, and hash computation maintains correct depth.
DLEQ math — Standard Schnorr-like proof of discrete log equality. The (n-e) trick for point negation (no OP_ECNEG opcode) is correct: (n-e)·P ≡ -e·P (mod n).
Point serialization in-script — y % 2 + 2 → prefix byte, NUM2BIN(x, 33) → REVERSEBYTES → RIGHT(32) → big-endian 32-byte X. Matches SEC1 compressed encoding. EC coordinates from OP_ECMUL are field elements (always non-negative), so parity check is safe.
Fiat-Shamir challenge consistency — The off-chain dleqScriptChallenge reverses the SHA256 digest bytes to match the script's LE→BIN2NUM interpretation, then reduces mod n. The script appends 0x00 before BIN2NUM to force positive sign-magnitude. Both produce the same scalar. Verified that the byte-reversal in dleqScriptChallenge is the correct transform (not the same as dleqChallenge in the pure-Go test, which uses BE — intentionally different).
Negative test coverage — Tampered s, tampered e, and a self-consistent proof for a different secret (which fails due to the in-script hash binding to A and C'). Good discrimination.
No production code changed. Both files are _test.go in the same package, sharing helpers correctly.
Clean. Approving the new commit.
🤖 Reviewed by Arkana
There was a problem hiding this comment.
Re-review — new commit 0a13dca (DLEQ script test)
Traced the full script construction in dleq_script_test.go opcode-by-opcode. Verified:
Stack depth tracking — The builder's depth variable and the pick helper correctly compute OP_PICK offsets for the three base items (e=0, s=1, n-e=2) throughout the entire script. Every scalarMul, ecAdd, serializeCompressed, alt-stack shuttle, and hash computation maintains correct depth.
DLEQ math — Standard Schnorr-like proof of discrete log equality. The (n-e) trick for point negation (no OP_ECNEG opcode) is correct: (n-e)*P = -e*P (mod n).
Point serialization in-script — y % 2 + 2 -> prefix byte, NUM2BIN(x, 33) -> REVERSEBYTES -> RIGHT(32) -> big-endian 32-byte X. Matches SEC1 compressed encoding. EC coordinates from OP_ECMUL are field elements (always non-negative), so parity check is safe.
Fiat-Shamir challenge consistency — The off-chain dleqScriptChallenge reverses the SHA256 digest bytes to match the script's LE BIN2NUM interpretation, then reduces mod n. The script appends 0x00 before BIN2NUM to force positive sign-magnitude. Both produce the same scalar. Verified that the byte-reversal in dleqScriptChallenge is the correct transform (not the same as dleqChallenge in the pure-Go test, which uses BE — intentionally different).
Negative test coverage — Tampered s, tampered e, and a self-consistent proof for a different secret (which fails due to the in-script hash binding to A and C'). Good discrimination.
No production code changed. Both files are _test.go in the same package, sharing helpers correctly.
Clean. Approving the new commit.
🤖 Reviewed by Arkana
Summary
Adds self-contained Go tests for a DLEQ (Discrete Log Equality) proof — the Cashu/BDHKE construction used to prove that a blinded signature was produced with a known public key, without revealing the secret.
Bob proves the same secret
alinks:A = a*G(the public key)C' = a*B'(the signature over Alice's blinded messageB')If accepted, the
ainA = a*Gequals theainC' = a*B'.What's included
Two complementary tests, no production code is modified — all logic lives in the test files:
1.
pkg/arkade/dleq_test.go— reference prove/verify in GoProve/verify helpers plus the test, built on the existing
btcec/v2point/scalar primitives already used intweak.go(ScalarBaseMultNonConst,ScalarMultNonConst,AddNonConst,ModNScalar).2.
pkg/arkade/dleq_script_test.go— verification inside an Arkade ScriptRuns the DLEQ verifier as an actual Arkade Script through the engine. The public instance
(G, A, B', C')is baked into the locking script; the proof(e, s)is supplied as the witness. The script:R1 = s*G - e*AandR2 = s*B' - e*C'withOP_ECMUL/OP_ECADD, using(n-e)*Pfor the subtraction (there is no point-negation opcode);(x, y)coordinates (OP_NUM2BIN/OP_REVERSEBYTES/OP_RIGHT/OP_CAT), which also bindsA/C'between the EC equations and the hash;OP_SHA256+OP_BIN2NUM+OP_MOD n, appending a synthesized0x00byte so the little-endian digest is never read as negative;e.Test cases
dleq_test.godleq_script_test.goAC'/ different secretseThe "different secret" case in the script test supplies a fully self-consistent
(e, s)for a differenta; it is rejected purely by the in-script hash binding to the script'sAandC', demonstrating soundness rather than blanket acceptance.Test plan
All subtests pass;
go vet ./...andgofmtare clean.