From cb43082d18d794668c445db2d4f6b913dae43ef3 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 21:28:12 +0000 Subject: [PATCH] feat(crypto): add Zarcanum verification context API Co-Authored-By: Charon --- crypto/bridge.cpp | 128 +++++++++++++++++++++++++++++++++++------- crypto/bridge.h | 14 ++++- crypto/crypto_test.go | 40 ++++++++++++- crypto/proof.go | 65 ++++++++++++++++++++- 4 files changed, 221 insertions(+), 26 deletions(-) diff --git a/crypto/bridge.cpp b/crypto/bridge.cpp index 31ab612..10082d7 100644 --- a/crypto/bridge.cpp +++ b/crypto/bridge.cpp @@ -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 stealth_keys(ring_size); + std::vector commitments(ring_size); + std::vector asset_ids(ring_size); + std::vector concealing_pts(ring_size); + std::vector 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 ────────────────────────────────── diff --git a/crypto/bridge.h b/crypto/bridge.h index 3a66179..47cbf8d 100644 --- a/crypto/bridge.h +++ b/crypto/bridge.h @@ -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) diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 365580c..fb0cd0c 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -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") } } diff --git a/crypto/proof.go b/crypto/proof.go index 1781136..f7bdb6d 100644 --- a/crypto/proof.go +++ b/crypto/proof.go @@ -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 +}