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 <charon@lethean.io>
This commit is contained in:
parent
a2df164822
commit
8e6dc326df
6 changed files with 222 additions and 3 deletions
18
consensus/balance.go
Normal file
18
consensus/balance.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue