feat(crypto): add generic double-Schnorr bridge
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run

Expose generate/verify wrappers for generic_double_schnorr_sig and add a consensus helper for balance-proof checks.

Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
Virgil 2026-04-04 18:26:33 +00:00
parent a2df164822
commit 8e6dc326df
6 changed files with 222 additions and 3 deletions

18
consensus/balance.go Normal file
View file

@ -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)
}

View file

@ -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
}

View file

@ -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<crypto::gt_X, crypto::gt_G>(
m, sa * crypto::c_point_X, sa, sb * crypto::c_point_G, sb, sig);
} else {
ok = crypto::generate_double_schnorr_sig<crypto::gt_G, crypto::gt_G>(
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<crypto::gt_X, crypto::gt_G>(m, a_pt, b_pk, sig) ? 0 : 1;
}
return crypto::verify_double_schnorr_sig<crypto::gt_G, crypto::gt_G>(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,

View file

@ -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).

View file

@ -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")
}
}

View file

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