feat(crypto): add Zarcanum verification context API
Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
2bebe323b8
commit
cb43082d18
4 changed files with 221 additions and 26 deletions
|
|
@ -104,34 +104,77 @@ bool deserialise_bpp(const uint8_t *buf, size_t len, crypto::bpp_signature &sig)
|
|||
return off == len; // must consume all bytes
|
||||
}
|
||||
|
||||
bool read_bppe_at(const uint8_t *buf, size_t len, size_t *offset,
|
||||
crypto::bppe_signature &sig) {
|
||||
if (!read_pubkey_vec(buf, len, offset, sig.L)) return false;
|
||||
if (!read_pubkey_vec(buf, len, offset, sig.R)) return false;
|
||||
if (!read_pubkey(buf, len, offset, sig.A0)) return false;
|
||||
if (!read_pubkey(buf, len, offset, sig.A)) return false;
|
||||
if (!read_pubkey(buf, len, offset, sig.B)) return false;
|
||||
if (!read_scalar(buf, len, offset, sig.r)) return false;
|
||||
if (!read_scalar(buf, len, offset, sig.s)) return false;
|
||||
if (!read_scalar(buf, len, offset, sig.delta_1)) return false;
|
||||
if (!read_scalar(buf, len, offset, sig.delta_2)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Deserialise a bppe_signature from wire bytes (Bulletproofs++ Enhanced, 2 deltas).
|
||||
// Layout: varint(len(L)) + L[]*32 + varint(len(R)) + R[]*32
|
||||
// + A0(32) + A(32) + B(32) + r(32) + s(32) + delta_1(32) + delta_2(32)
|
||||
bool deserialise_bppe(const uint8_t *buf, size_t len, crypto::bppe_signature &sig) {
|
||||
size_t off = 0;
|
||||
if (!read_pubkey_vec(buf, len, &off, sig.L)) return false;
|
||||
if (!read_pubkey_vec(buf, len, &off, sig.R)) return false;
|
||||
if (!read_pubkey(buf, len, &off, sig.A0)) return false;
|
||||
if (!read_pubkey(buf, len, &off, sig.A)) return false;
|
||||
if (!read_pubkey(buf, len, &off, sig.B)) return false;
|
||||
if (!read_scalar(buf, len, &off, sig.r)) return false;
|
||||
if (!read_scalar(buf, len, &off, sig.s)) return false;
|
||||
if (!read_scalar(buf, len, &off, sig.delta_1)) return false;
|
||||
if (!read_scalar(buf, len, &off, sig.delta_2)) return false;
|
||||
if (!read_bppe_at(buf, len, &off, sig)) return false;
|
||||
return off == len; // must consume all bytes
|
||||
}
|
||||
|
||||
bool read_bge_at(const uint8_t *buf, size_t len, size_t *offset,
|
||||
crypto::BGE_proof &proof) {
|
||||
if (!read_pubkey(buf, len, offset, proof.A)) return false;
|
||||
if (!read_pubkey(buf, len, offset, proof.B)) return false;
|
||||
if (!read_pubkey_vec(buf, len, offset, proof.Pk)) return false;
|
||||
if (!read_scalar_vec(buf, len, offset, proof.f)) return false;
|
||||
if (!read_scalar(buf, len, offset, proof.y)) return false;
|
||||
if (!read_scalar(buf, len, offset, proof.z)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Deserialise a BGE_proof from wire bytes.
|
||||
// Layout: A(32) + B(32) + varint(len(Pk)) + Pk[]*32
|
||||
// + varint(len(f)) + f[]*32 + y(32) + z(32)
|
||||
bool deserialise_bge(const uint8_t *buf, size_t len, crypto::BGE_proof &proof) {
|
||||
size_t off = 0;
|
||||
if (!read_pubkey(buf, len, &off, proof.A)) return false;
|
||||
if (!read_pubkey(buf, len, &off, proof.B)) return false;
|
||||
if (!read_pubkey_vec(buf, len, &off, proof.Pk)) return false;
|
||||
if (!read_scalar_vec(buf, len, &off, proof.f)) return false;
|
||||
if (!read_scalar(buf, len, &off, proof.y)) return false;
|
||||
if (!read_scalar(buf, len, &off, proof.z)) return false;
|
||||
if (!read_bge_at(buf, len, &off, proof)) return false;
|
||||
return off == len;
|
||||
}
|
||||
|
||||
bool read_clsag_ggxxg_at(const uint8_t *buf, size_t len, size_t *offset,
|
||||
crypto::CLSAG_GGXXG_signature &sig) {
|
||||
if (!read_scalar(buf, len, offset, sig.c)) return false;
|
||||
if (!read_scalar_vec(buf, len, offset, sig.r_g)) return false;
|
||||
if (!read_scalar_vec(buf, len, offset, sig.r_x)) return false;
|
||||
if (!read_pubkey(buf, len, offset, sig.K1)) return false;
|
||||
if (!read_pubkey(buf, len, offset, sig.K2)) return false;
|
||||
if (!read_pubkey(buf, len, offset, sig.K3)) return false;
|
||||
if (!read_pubkey(buf, len, offset, sig.K4)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool deserialise_zarcanum(const uint8_t *buf, size_t len,
|
||||
crypto::zarcanum_proof &proof) {
|
||||
size_t off = 0;
|
||||
if (!read_scalar(buf, len, &off, proof.d)) return false;
|
||||
if (!read_pubkey(buf, len, &off, proof.C)) return false;
|
||||
if (!read_pubkey(buf, len, &off, proof.C_prime)) return false;
|
||||
if (!read_pubkey(buf, len, &off, proof.E)) return false;
|
||||
if (!read_scalar(buf, len, &off, proof.c)) return false;
|
||||
if (!read_scalar(buf, len, &off, proof.y0)) return false;
|
||||
if (!read_scalar(buf, len, &off, proof.y1)) return false;
|
||||
if (!read_scalar(buf, len, &off, proof.y2)) return false;
|
||||
if (!read_scalar(buf, len, &off, proof.y3)) return false;
|
||||
if (!read_scalar(buf, len, &off, proof.y4)) return false;
|
||||
if (!read_bppe_at(buf, len, &off, proof.E_range_proof)) return false;
|
||||
if (!read_pubkey(buf, len, &off, proof.pseudo_out_amount_commitment)) return false;
|
||||
if (!read_clsag_ggxxg_at(buf, len, &off, proof.clsag_ggxxg)) return false;
|
||||
return off == len;
|
||||
}
|
||||
|
||||
|
|
@ -724,12 +767,59 @@ int cn_double_schnorr_verify(int a_is_x, const uint8_t hash[32],
|
|||
}
|
||||
|
||||
// ── Zarcanum PoS ────────────────────────────────────────
|
||||
// Zarcanum verification requires many parameters beyond what the current
|
||||
// bridge API exposes (kernel_hash, ring, last_pow_block_id, stake_ki,
|
||||
// pos_difficulty). Returns -1 until the API is extended.
|
||||
// Compatibility wrapper for the historical proof-only API.
|
||||
int cn_zarcanum_verify(const uint8_t /*hash*/[32], const uint8_t * /*proof*/,
|
||||
size_t /*proof_len*/) {
|
||||
return -1; // needs extended API — see bridge.h TODO
|
||||
return -1;
|
||||
}
|
||||
|
||||
int cn_zarcanum_verify_full(const uint8_t m[32], const uint8_t kernel_hash[32],
|
||||
const uint8_t *ring, size_t ring_size,
|
||||
const uint8_t last_pow_block_id_hashed[32],
|
||||
const uint8_t stake_ki[32],
|
||||
uint64_t pos_difficulty,
|
||||
const uint8_t *proof, size_t proof_len) {
|
||||
if (m == nullptr || kernel_hash == nullptr || ring == nullptr ||
|
||||
last_pow_block_id_hashed == nullptr || stake_ki == nullptr ||
|
||||
proof == nullptr || proof_len == 0 || ring_size == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
crypto::hash msg;
|
||||
crypto::hash kernel;
|
||||
crypto::scalar_t last_pow;
|
||||
crypto::key_image key_img;
|
||||
memcpy(&msg, m, 32);
|
||||
memcpy(&kernel, kernel_hash, 32);
|
||||
memcpy(&last_pow, last_pow_block_id_hashed, 32);
|
||||
memcpy(&key_img, stake_ki, 32);
|
||||
|
||||
std::vector<crypto::public_key> stealth_keys(ring_size);
|
||||
std::vector<crypto::public_key> commitments(ring_size);
|
||||
std::vector<crypto::public_key> asset_ids(ring_size);
|
||||
std::vector<crypto::public_key> concealing_pts(ring_size);
|
||||
std::vector<crypto::CLSAG_GGXXG_input_ref_t> ring_refs;
|
||||
ring_refs.reserve(ring_size);
|
||||
for (size_t i = 0; i < ring_size; ++i) {
|
||||
memcpy(&stealth_keys[i], ring + i * 128, 32);
|
||||
memcpy(&commitments[i], ring + i * 128 + 32, 32);
|
||||
memcpy(&asset_ids[i], ring + i * 128 + 64, 32);
|
||||
memcpy(&concealing_pts[i], ring + i * 128 + 96, 32);
|
||||
ring_refs.emplace_back(stealth_keys[i], commitments[i], asset_ids[i], concealing_pts[i]);
|
||||
}
|
||||
|
||||
crypto::zarcanum_proof sig;
|
||||
if (!deserialise_zarcanum(proof, proof_len, sig)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
crypto::mp::uint128_t difficulty(pos_difficulty);
|
||||
return crypto::zarcanum_verify_proof(msg, kernel, ring_refs, last_pow,
|
||||
key_img, difficulty, sig) ? 0 : 1;
|
||||
} catch (...) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ── RandomX PoW Hashing ──────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -146,11 +146,21 @@ int cn_double_schnorr_verify(int a_is_x, const uint8_t hash[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).
|
||||
// Legacy compatibility wrapper for the historical proof-only API.
|
||||
int cn_zarcanum_verify(const uint8_t hash[32], const uint8_t *proof,
|
||||
size_t proof_len);
|
||||
|
||||
// Full Zarcanum verification entrypoint.
|
||||
// ring is a flat array of 128-byte CLSAG_GGXXG ring members:
|
||||
// [stealth(32) | amount_commitment(32) | blinded_asset_id(32) | concealing(32)]
|
||||
// Returns 0 on success, 1 on verification failure or deserialisation error.
|
||||
int cn_zarcanum_verify_full(const uint8_t m[32], const uint8_t kernel_hash[32],
|
||||
const uint8_t *ring, size_t ring_size,
|
||||
const uint8_t last_pow_block_id_hashed[32],
|
||||
const uint8_t stake_ki[32],
|
||||
uint64_t pos_difficulty,
|
||||
const uint8_t *proof, size_t proof_len);
|
||||
|
||||
// ── RandomX PoW Hashing ──────────────────────────────────
|
||||
// key/key_size: RandomX cache key (e.g. "LetheanRandomXv1")
|
||||
// input/input_size: block header hash (32 bytes) + nonce (8 bytes LE)
|
||||
|
|
|
|||
|
|
@ -578,11 +578,45 @@ func TestBGE_Bad_GarbageProof(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestZarcanum_Stub_NotImplemented(t *testing.T) {
|
||||
// Zarcanum bridge API needs extending — verify it returns false.
|
||||
func TestZarcanumCompatibilityWrapper_Bad_EmptyProof(t *testing.T) {
|
||||
hash := [32]byte{0x01}
|
||||
if crypto.VerifyZarcanum(hash, []byte{0x00}) {
|
||||
t.Fatal("Zarcanum stub should return false")
|
||||
t.Fatal("compatibility wrapper should reject malformed proof data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZarcanumWithContext_Bad_MinimalProof(t *testing.T) {
|
||||
var ctx crypto.ZarcanumVerificationContext
|
||||
ctx.ContextHash = [32]byte{0x01}
|
||||
ctx.KernelHash = [32]byte{0x02}
|
||||
ctx.LastPowBlockIDHashed = [32]byte{0x03}
|
||||
ctx.StakeKeyImage = [32]byte{0x04}
|
||||
ctx.PosDifficulty = 1
|
||||
ctx.Ring = []crypto.ZarcanumRingMember{{
|
||||
StealthAddress: [32]byte{0x11},
|
||||
AmountCommitment: [32]byte{0x22},
|
||||
BlindedAssetID: [32]byte{0x33},
|
||||
ConcealingPoint: [32]byte{0x44},
|
||||
}}
|
||||
|
||||
// Minimal structurally valid proof blob:
|
||||
// 10 scalars/points + empty BPPE + pseudo_out_amount_commitment +
|
||||
// CLSAG_GGXXG with one ring entry and zeroed scalars.
|
||||
proof := make([]byte, 0, 10*32+2+32+2+32+1+128)
|
||||
proof = append(proof, make([]byte, 10*32)...)
|
||||
proof = append(proof, 0x00) // BPPE L length
|
||||
proof = append(proof, 0x00) // BPPE R length
|
||||
proof = append(proof, make([]byte, 7*32)...)
|
||||
proof = append(proof, make([]byte, 32)...)
|
||||
proof = append(proof, 0x01) // CLSAG_GGXXG r_g length
|
||||
proof = append(proof, make([]byte, 32)...)
|
||||
proof = append(proof, 0x01) // CLSAG_GGXXG r_x length
|
||||
proof = append(proof, make([]byte, 32)...)
|
||||
proof = append(proof, make([]byte, 128)...)
|
||||
ctx.Proof = proof
|
||||
|
||||
if crypto.VerifyZarcanumWithContext(ctx) {
|
||||
t.Fatal("minimal Zarcanum proof should fail verification")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,27 @@ import (
|
|||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// ZarcanumRingMember is one flat ring entry for Zarcanum verification.
|
||||
// All fields are stored premultiplied by 1/8, matching the on-chain form.
|
||||
type ZarcanumRingMember struct {
|
||||
StealthAddress [32]byte
|
||||
AmountCommitment [32]byte
|
||||
BlindedAssetID [32]byte
|
||||
ConcealingPoint [32]byte
|
||||
}
|
||||
|
||||
// ZarcanumVerificationContext groups the full context required by the
|
||||
// upstream C++ verifier.
|
||||
type ZarcanumVerificationContext struct {
|
||||
ContextHash [32]byte
|
||||
KernelHash [32]byte
|
||||
Ring []ZarcanumRingMember
|
||||
LastPowBlockIDHashed [32]byte
|
||||
StakeKeyImage [32]byte
|
||||
Proof []byte
|
||||
PosDifficulty uint64
|
||||
}
|
||||
|
||||
// GenerateDoubleSchnorr creates a generic_double_schnorr_sig from zarcanum.h.
|
||||
// aIsX selects the generator pair:
|
||||
//
|
||||
|
|
@ -133,8 +154,8 @@ func VerifyDoubleSchnorr(hash [32]byte, aIsX bool, a [32]byte, b [32]byte, proof
|
|||
}
|
||||
|
||||
// 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.
|
||||
// This compatibility wrapper remains for the historical proof blob API.
|
||||
// Use VerifyZarcanumWithContext for full verification.
|
||||
func VerifyZarcanum(hash [32]byte, proof []byte) bool {
|
||||
if len(proof) == 0 {
|
||||
return false
|
||||
|
|
@ -145,3 +166,43 @@ func VerifyZarcanum(hash [32]byte, proof []byte) bool {
|
|||
C.size_t(len(proof)),
|
||||
) == 0
|
||||
}
|
||||
|
||||
// VerifyZarcanumWithContext verifies a Zarcanum PoS proof with the full
|
||||
// consensus context required by the upstream verifier.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// crypto.VerifyZarcanumWithContext(crypto.ZarcanumVerificationContext{
|
||||
// ContextHash: txHash,
|
||||
// KernelHash: kernelHash,
|
||||
// Ring: ring,
|
||||
// LastPowBlockIDHashed: lastPowHash,
|
||||
// StakeKeyImage: stakeKeyImage,
|
||||
// PosDifficulty: posDifficulty,
|
||||
// Proof: proofBlob,
|
||||
// })
|
||||
func VerifyZarcanumWithContext(ctx ZarcanumVerificationContext) bool {
|
||||
if len(ctx.Ring) == 0 || len(ctx.Proof) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
flat := make([]byte, len(ctx.Ring)*128)
|
||||
for i, member := range ctx.Ring {
|
||||
copy(flat[i*128:], member.StealthAddress[:])
|
||||
copy(flat[i*128+32:], member.AmountCommitment[:])
|
||||
copy(flat[i*128+64:], member.BlindedAssetID[:])
|
||||
copy(flat[i*128+96:], member.ConcealingPoint[:])
|
||||
}
|
||||
|
||||
return C.cn_zarcanum_verify_full(
|
||||
(*C.uint8_t)(unsafe.Pointer(&ctx.ContextHash[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&ctx.KernelHash[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&flat[0])),
|
||||
C.size_t(len(ctx.Ring)),
|
||||
(*C.uint8_t)(unsafe.Pointer(&ctx.LastPowBlockIDHashed[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&ctx.StakeKeyImage[0])),
|
||||
C.uint64_t(ctx.PosDifficulty),
|
||||
(*C.uint8_t)(unsafe.Pointer(&ctx.Proof[0])),
|
||||
C.size_t(len(ctx.Proof)),
|
||||
) == 0
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue