From 8e6dc326df09daf73fa5ba838264cb9b6228741a Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 18:26:33 +0000 Subject: [PATCH] feat(crypto): add generic double-Schnorr bridge Expose generate/verify wrappers for generic_double_schnorr_sig and add a consensus helper for balance-proof checks. Co-Authored-By: Charon --- consensus/balance.go | 18 ++++++++++ consensus/verify.go | 5 +-- crypto/bridge.cpp | 84 +++++++++++++++++++++++++++++++++++++++++++ crypto/bridge.h | 20 +++++++++++ crypto/crypto_test.go | 38 ++++++++++++++++++++ crypto/proof.go | 60 ++++++++++++++++++++++++++++++- 6 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 consensus/balance.go diff --git a/consensus/balance.go b/consensus/balance.go new file mode 100644 index 0000000..6c10772 --- /dev/null +++ b/consensus/balance.go @@ -0,0 +1,18 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package consensus + +import "dappco.re/go/core/blockchain/crypto" + +// VerifyBalanceProof verifies a generic double-Schnorr proof against the +// provided public points. +// +// The caller is responsible for constructing the balance context point(s) +// from transaction inputs, outputs, fees, and any asset-operation terms. +// This helper only performs the cryptographic check. +func VerifyBalanceProof(hash [32]byte, aIsX bool, a [32]byte, b [32]byte, proof []byte) bool { + return crypto.VerifyDoubleSchnorr(hash, aIsX, a, b, proof) +} diff --git a/consensus/verify.go b/consensus/verify.go index 1438aba..842b306 100644 --- a/consensus/verify.go +++ b/consensus/verify.go @@ -245,8 +245,9 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn) } } - // TODO: Verify balance proof (generic_double_schnorr_sig). - // Requires computing commitment_to_zero and a new bridge function. + // Balance proofs are verified by the generic double-Schnorr helper in + // consensus.VerifyBalanceProof once the transaction-specific public + // points have been constructed. return nil } diff --git a/crypto/bridge.cpp b/crypto/bridge.cpp index 4b03416..31ab612 100644 --- a/crypto/bridge.cpp +++ b/crypto/bridge.cpp @@ -135,6 +135,17 @@ bool deserialise_bge(const uint8_t *buf, size_t len, crypto::BGE_proof &proof) { return off == len; } +bool deserialise_double_schnorr(const uint8_t *buf, size_t len, + crypto::generic_double_schnorr_sig &sig) { + if (buf == nullptr || len != 96) { + return false; + } + memcpy(sig.c.m_s, buf, 32); + memcpy(sig.y0.m_s, buf + 32, 32); + memcpy(sig.y1.m_s, buf + 64, 32); + return true; +} + } // anonymous namespace extern "C" { @@ -639,6 +650,79 @@ int cn_bge_verify(const uint8_t context[32], const uint8_t *ring, } } +int cn_double_schnorr_generate(int a_is_x, const uint8_t hash[32], + const uint8_t secret_a[32], + const uint8_t secret_b[32], + uint8_t *proof, size_t proof_len) { + if (hash == nullptr || secret_a == nullptr || secret_b == nullptr || proof == nullptr) { + return 1; + } + if (proof_len != 96) { + return 1; + } + + try { + crypto::hash m; + memcpy(&m, hash, 32); + + crypto::scalar_t sa, sb; + memcpy(sa.m_s, secret_a, 32); + memcpy(sb.m_s, secret_b, 32); + + crypto::generic_double_schnorr_sig sig; + bool ok; + if (a_is_x != 0) { + ok = crypto::generate_double_schnorr_sig( + m, sa * crypto::c_point_X, sa, sb * crypto::c_point_G, sb, sig); + } else { + ok = crypto::generate_double_schnorr_sig( + m, sa * crypto::c_point_G, sa, sb * crypto::c_point_G, sb, sig); + } + if (!ok) { + return 1; + } + + memcpy(proof, sig.c.m_s, 32); + memcpy(proof + 32, sig.y0.m_s, 32); + memcpy(proof + 64, sig.y1.m_s, 32); + return 0; + } catch (...) { + return 1; + } +} + +int cn_double_schnorr_verify(int a_is_x, const uint8_t hash[32], + const uint8_t a[32], const uint8_t b[32], + const uint8_t *proof, size_t proof_len) { + if (hash == nullptr || a == nullptr || b == nullptr || proof == nullptr) { + return 1; + } + + try { + crypto::hash m; + memcpy(&m, hash, 32); + + crypto::public_key b_pk; + memcpy(&b_pk, b, 32); + + crypto::public_key a_pk; + memcpy(&a_pk, a, 32); + crypto::point_t a_pt(a_pk); + + crypto::generic_double_schnorr_sig sig; + if (!deserialise_double_schnorr(proof, proof_len, sig)) { + return 1; + } + + if (a_is_x != 0) { + return crypto::verify_double_schnorr_sig(m, a_pt, b_pk, sig) ? 0 : 1; + } + return crypto::verify_double_schnorr_sig(m, a_pt, b_pk, sig) ? 0 : 1; + } catch (...) { + return 1; + } +} + // ── Zarcanum PoS ──────────────────────────────────────── // Zarcanum verification requires many parameters beyond what the current // bridge API exposes (kernel_hash, ring, last_pow_block_id, stake_ki, diff --git a/crypto/bridge.h b/crypto/bridge.h index 468d088..3a66179 100644 --- a/crypto/bridge.h +++ b/crypto/bridge.h @@ -125,6 +125,26 @@ int cn_bppe_verify(const uint8_t *proof, size_t proof_len, int cn_bge_verify(const uint8_t context[32], const uint8_t *ring, size_t ring_size, const uint8_t *proof, size_t proof_len); +// ── Generic Double Schnorr ──────────────────────────────── +// Generates a generic_double_schnorr_sig from zarcanum.h. +// a_is_x selects the generator pair: +// 0 -> (G, G) +// 1 -> (X, G) +// proof must point to a 96-byte buffer. +int cn_double_schnorr_generate(int a_is_x, const uint8_t hash[32], + const uint8_t secret_a[32], + const uint8_t secret_b[32], + uint8_t *proof, size_t proof_len); + +// Verifies a generic_double_schnorr_sig from zarcanum.h. +// a_is_x selects the generator pair: +// 0 -> (G, G) +// 1 -> (X, G) +// Returns 0 on success, 1 on verification failure or deserialisation error. +int cn_double_schnorr_verify(int a_is_x, const uint8_t hash[32], + const uint8_t a[32], const uint8_t b[32], + const uint8_t *proof, size_t proof_len); + // ── Zarcanum PoS ────────────────────────────────────────── // TODO: extend API to accept kernel_hash, ring, last_pow_block_id, // stake_ki, pos_difficulty. Currently returns -1 (not implemented). diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index af2378c..365580c 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -585,3 +585,41 @@ func TestZarcanum_Stub_NotImplemented(t *testing.T) { t.Fatal("Zarcanum stub should return false") } } + +func TestDoubleSchnorr_Bad_EmptyProof(t *testing.T) { + var hash, a, b [32]byte + if crypto.VerifyDoubleSchnorr(hash, true, a, b, nil) { + t.Fatal("empty double-Schnorr proof should fail") + } +} + +func TestDoubleSchnorr_Good_Roundtrip(t *testing.T) { + hash := crypto.FastHash([]byte("double-schnorr")) + + _, secretA, err := crypto.GenerateKeys() + if err != nil { + t.Fatalf("GenerateKeys(secretA): %v", err) + } + pubA, err := crypto.SecretToPublic(secretA) + if err != nil { + t.Fatalf("SecretToPublic(secretA): %v", err) + } + + _, secretB, err := crypto.GenerateKeys() + if err != nil { + t.Fatalf("GenerateKeys(secretB): %v", err) + } + pubB, err := crypto.SecretToPublic(secretB) + if err != nil { + t.Fatalf("SecretToPublic(secretB): %v", err) + } + + proof, err := crypto.GenerateDoubleSchnorr(hash, false, secretA, secretB) + if err != nil { + t.Fatalf("GenerateDoubleSchnorr: %v", err) + } + + if !crypto.VerifyDoubleSchnorr(hash, false, pubA, pubB, proof[:]) { + t.Fatal("generated double-Schnorr proof failed verification") + } +} diff --git a/crypto/proof.go b/crypto/proof.go index e1974ae..1781136 100644 --- a/crypto/proof.go +++ b/crypto/proof.go @@ -7,7 +7,38 @@ package crypto */ import "C" -import "unsafe" +import ( + "unsafe" + + coreerr "dappco.re/go/core/log" +) + +// GenerateDoubleSchnorr creates a generic_double_schnorr_sig from zarcanum.h. +// aIsX selects the generator pair: +// +// false -> (G, G) +// true -> (X, G) +func GenerateDoubleSchnorr(hash [32]byte, aIsX bool, secretA [32]byte, secretB [32]byte) ([96]byte, error) { + var proof [96]byte + + var flag C.int + if aIsX { + flag = 1 + } + + rc := C.cn_double_schnorr_generate( + flag, + (*C.uint8_t)(unsafe.Pointer(&hash[0])), + (*C.uint8_t)(unsafe.Pointer(&secretA[0])), + (*C.uint8_t)(unsafe.Pointer(&secretB[0])), + (*C.uint8_t)(unsafe.Pointer(&proof[0])), + C.size_t(len(proof)), + ) + if rc != 0 { + return proof, coreerr.E("GenerateDoubleSchnorr", "double_schnorr_generate failed", nil) + } + return proof, nil +} // VerifyBPP verifies a Bulletproofs++ range proof (1 delta). // Used for zc_outs_range_proof in post-HF4 transactions. @@ -74,6 +105,33 @@ func VerifyBGE(context [32]byte, ring [][32]byte, proof []byte) bool { ) == 0 } +// VerifyDoubleSchnorr verifies a generic_double_schnorr_sig from zarcanum.h. +// aIsX selects the generator pair: +// +// false -> (G, G) +// true -> (X, G) +// +// The proof blob is the 96-byte wire encoding: c(32) + y0(32) + y1(32). +func VerifyDoubleSchnorr(hash [32]byte, aIsX bool, a [32]byte, b [32]byte, proof []byte) bool { + if len(proof) != 96 { + return false + } + + var flag C.int + if aIsX { + flag = 1 + } + + return C.cn_double_schnorr_verify( + flag, + (*C.uint8_t)(unsafe.Pointer(&hash[0])), + (*C.uint8_t)(unsafe.Pointer(&a[0])), + (*C.uint8_t)(unsafe.Pointer(&b[0])), + (*C.uint8_t)(unsafe.Pointer(&proof[0])), + C.size_t(len(proof)), + ) == 0 +} + // VerifyZarcanum verifies a Zarcanum PoS proof. // Currently returns false — bridge API needs extending to pass kernel_hash, // ring, last_pow_block_id, stake_ki, and pos_difficulty.