Skip to content

test: DLEQ proof for blinded-signature key equality#95

Open
Kukks wants to merge 2 commits into
masterfrom
test/dleq-proof
Open

test: DLEQ proof for blinded-signature key equality#95
Kukks wants to merge 2 commits into
masterfrom
test/dleq-proof

Conversation

@Kukks

@Kukks Kukks commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

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 a links:

  • A = a*G (the public key)
  • C' = a*B' (the signature over Alice's blinded message B')
Bob:   r random; R1 = r*G; R2 = r*B'; e = H(R1,R2,A,C'); s = r + e*a  →  (e, s)
Alice: R1 = s*G - e*A; R2 = s*B' - e*C'; accept iff e == H(R1,R2,A,C')

If accepted, the a in A = a*G equals the a in C' = 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 Go

Prove/verify helpers plus the test, built on the existing btcec/v2 point/scalar primitives already used in tweak.go (ScalarBaseMultNonConst, ScalarMultNonConst, AddNonConst, ModNScalar).

2. pkg/arkade/dleq_script_test.go — verification inside an Arkade Script

Runs 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:

  • recomputes R1 = s*G - e*A and R2 = s*B' - e*C' with OP_ECMUL/OP_ECADD, using (n-e)*P for the subtraction (there is no point-negation opcode);
  • reconstructs the SEC1 compressed points from their affine (x, y) coordinates (OP_NUM2BIN/OP_REVERSEBYTES/OP_RIGHT/OP_CAT), which also binds A/C' between the EC equations and the hash;
  • derives the Fiat-Shamir challenge with OP_SHA256 + OP_BIN2NUM + OP_MOD n, appending a synthesized 0x00 byte so the little-endian digest is never read as negative;
  • asserts the recomputed challenge equals the witness e.

Test cases

Case dleq_test.go dleq_script_test.go
Valid proof ✅ accept ✅ accept
Wrong A ❌ reject
Wrong C' / different secret ❌ reject ❌ reject
Tampered s ❌ reject ❌ reject
Tampered e ❌ reject ❌ reject

The "different secret" case in the script test supplies a fully self-consistent (e, s) for a different a; it is rejected purely by the in-script hash binding to the script's A and C', demonstrating soundness rather than blanket acceptance.

Test plan

cd pkg/arkade && go test . -run 'TestDLEQProof|TestDLEQScript' -v

All subtests pass; go vet ./... and gofmt are clean.

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.

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — Approved

Clean, self-contained test. No production code changed. DLEQ math is correct.

Verified

  • Proof/verify algebra: s*G - e*A = r*G and s*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/v2 point/scalar ops consistent with tweak.go and ec_ops.go.

Minor notes (non-blocking)

  1. mustPrivKeyFromSeed near-duplicate (dleq_test.go:65): tweak_test.go:96 already has mustPrivKeyFromSeedWithPrefix which does the same core thing (SHA-256 seed → PrivKeyFromBytes). Could reuse it with a don't-care prefix, but not worth blocking over.

  2. SetBytes overflow ignored (dleq_test.go:82, dleq_test.go:116): ModNScalar.SetBytes silently 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.

  3. 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.

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — Approved

Clean, self-contained test. No production code changed. DLEQ math is correct.

Verified

  • Proof/verify algebra: s*G - e*A = r*G and s*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/v2 point/scalar ops consistent with tweak.go and ec_ops.go.

Minor notes (non-blocking)

  1. mustPrivKeyFromSeed near-duplicate (dleq_test.go:65): tweak_test.go:96 already has mustPrivKeyFromSeedWithPrefix which does the same core thing (SHA-256 seed → PrivKeyFromBytes). Could reuse it with a don't-care prefix, but not worth blocking over.

  2. SetBytes overflow ignored (dleq_test.go:82, dleq_test.go:116): ModNScalar.SetBytes silently 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.

  3. 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').

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-scripty % 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

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-scripty % 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant