feat(consensus): verify HF4 balance proofs
Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
09729c0aeb
commit
0de491b394
6 changed files with 370 additions and 10 deletions
|
|
@ -7,6 +7,7 @@ package consensus
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -26,8 +27,9 @@ type zcSigData struct {
|
|||
|
||||
// v2SigEntry is one parsed entry from the V2 signature variant vector.
|
||||
type v2SigEntry struct {
|
||||
tag uint8
|
||||
zcSig *zcSigData // non-nil when tag == SigTypeZC
|
||||
tag uint8
|
||||
zcSig *zcSigData // non-nil when tag == SigTypeZC
|
||||
pseudoOutCommitment *[32]byte
|
||||
}
|
||||
|
||||
// parseV2Signatures parses the SignaturesRaw variant vector into a slice
|
||||
|
|
@ -59,6 +61,7 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
|
|||
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("parse ZC_sig %d", i), err)
|
||||
}
|
||||
entry.zcSig = zc
|
||||
entry.pseudoOutCommitment = &zc.pseudoOutCommitment
|
||||
|
||||
case types.SigTypeVoid:
|
||||
// Empty struct — nothing to read.
|
||||
|
|
@ -71,9 +74,11 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
|
|||
}
|
||||
|
||||
case types.SigTypeZarcanum:
|
||||
// Skip: 10 scalars + bppe + public_key + CLSAG_GGXXG.
|
||||
// Use skipZarcanumSig to advance past the data.
|
||||
skipZarcanumSig(dec)
|
||||
pseudoOut, err := parseZarcanumSigPseudoOut(dec)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("parse zarcanum_sig %d", i), err)
|
||||
}
|
||||
entry.pseudoOutCommitment = &pseudoOut
|
||||
|
||||
default:
|
||||
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("unsupported sig tag 0x%02x", tag), nil)
|
||||
|
|
@ -142,9 +147,12 @@ func parseZCSig(dec *wire.Decoder) (*zcSigData, error) {
|
|||
return &zc, nil
|
||||
}
|
||||
|
||||
// skipZarcanumSig advances the decoder past a zarcanum_sig element.
|
||||
// parseZarcanumSigPseudoOut advances past a zarcanum_sig while preserving
|
||||
// its pseudo-out amount commitment for balance-proof verification.
|
||||
// Wire: 10 scalars + bppe_serialized + public_key(32) + CLSAG_GGXXG.
|
||||
func skipZarcanumSig(dec *wire.Decoder) {
|
||||
func parseZarcanumSigPseudoOut(dec *wire.Decoder) ([32]byte, error) {
|
||||
var pseudoOut [32]byte
|
||||
|
||||
// 10 fixed scalars/points (320 bytes).
|
||||
_ = dec.ReadBytes(10 * 32)
|
||||
|
||||
|
|
@ -154,13 +162,22 @@ func skipZarcanumSig(dec *wire.Decoder) {
|
|||
_ = dec.ReadBytes(7 * 32)
|
||||
|
||||
// pseudo_out_amount_commitment (32 bytes).
|
||||
_ = dec.ReadBytes(32)
|
||||
dec.ReadBlob32(&pseudoOut)
|
||||
if dec.Err() != nil {
|
||||
return pseudoOut, dec.Err()
|
||||
}
|
||||
|
||||
// CLSAG_GGXXG: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32) + K3(32) + K4(32).
|
||||
_ = dec.ReadBytes(32) // c
|
||||
skipVecOfPoints(dec) // r_g
|
||||
skipVecOfPoints(dec) // r_x
|
||||
_ = dec.ReadBytes(128) // K1+K2+K3+K4
|
||||
|
||||
if dec.Err() != nil {
|
||||
return pseudoOut, dec.Err()
|
||||
}
|
||||
|
||||
return pseudoOut, nil
|
||||
}
|
||||
|
||||
// skipVecOfPoints advances the decoder past a varint(count) + count*32 vector.
|
||||
|
|
@ -246,6 +263,91 @@ func parseV2Proofs(raw []byte) (*v2ProofData, error) {
|
|||
return &data, nil
|
||||
}
|
||||
|
||||
type v2ExtraMetadata struct {
|
||||
txPublicKey [32]byte
|
||||
hasTxPubKey bool
|
||||
fee uint64
|
||||
hasFee bool
|
||||
}
|
||||
|
||||
func parseV2ExtraMetadata(raw []byte) (*v2ExtraMetadata, error) {
|
||||
if len(raw) == 0 {
|
||||
return &v2ExtraMetadata{}, nil
|
||||
}
|
||||
|
||||
dec := wire.NewDecoder(bytes.NewReader(raw))
|
||||
count := dec.ReadVarint()
|
||||
if dec.Err() != nil {
|
||||
return nil, coreerr.E("parseV2ExtraMetadata", "read extra count", dec.Err())
|
||||
}
|
||||
|
||||
var meta v2ExtraMetadata
|
||||
for i := uint64(0); i < count; i++ {
|
||||
tag := dec.ReadUint8()
|
||||
if dec.Err() != nil {
|
||||
return nil, coreerr.E("parseV2ExtraMetadata", fmt.Sprintf("read extra tag %d", i), dec.Err())
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case 22: // public_key
|
||||
dec.ReadBlob32(&meta.txPublicKey)
|
||||
meta.hasTxPubKey = true
|
||||
case 39: // zarcanum_tx_data_v1.fee — 8-byte little-endian
|
||||
feeBytes := dec.ReadBytes(8)
|
||||
if dec.Err() == nil {
|
||||
meta.fee = binary.LittleEndian.Uint64(feeBytes)
|
||||
meta.hasFee = true
|
||||
}
|
||||
default:
|
||||
if err := skipExtraVariantElement(dec, tag); err != nil {
|
||||
return nil, coreerr.E("parseV2ExtraMetadata", fmt.Sprintf("parse extra tag 0x%02x", tag), err)
|
||||
}
|
||||
}
|
||||
|
||||
if dec.Err() != nil {
|
||||
return nil, coreerr.E("parseV2ExtraMetadata", fmt.Sprintf("parse extra tag 0x%02x", tag), dec.Err())
|
||||
}
|
||||
}
|
||||
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func skipExtraVariantElement(dec *wire.Decoder, tag uint8) error {
|
||||
switch tag {
|
||||
case 7, 9, 11, 19:
|
||||
skipVarintBytes(dec)
|
||||
case 10:
|
||||
_ = dec.ReadBytes(8)
|
||||
case 14, 15, 16, 26, 27:
|
||||
_ = dec.ReadVarint()
|
||||
case 17, 28:
|
||||
_ = dec.ReadBytes(4)
|
||||
case 18:
|
||||
skipVarintBytes(dec)
|
||||
_ = dec.ReadBytes(32)
|
||||
_ = dec.ReadVarint()
|
||||
case 20, 21, 24, 31, 32, 33, 40:
|
||||
skipVarintBytes(dec)
|
||||
case 23:
|
||||
_ = dec.ReadBytes(2)
|
||||
case 25:
|
||||
_ = dec.ReadVarint()
|
||||
case 29:
|
||||
_ = dec.ReadBytes(64)
|
||||
default:
|
||||
return fmt.Errorf("unsupported extra tag 0x%02x", tag)
|
||||
}
|
||||
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
func skipVarintBytes(dec *wire.Decoder) {
|
||||
n := dec.ReadVarint()
|
||||
if dec.Err() == nil && n > 0 {
|
||||
_ = dec.ReadBytes(int(n))
|
||||
}
|
||||
}
|
||||
|
||||
// readBGEProofBytes reads a BGE_proof_s and returns the raw wire bytes.
|
||||
// Wire: A(32) + B(32) + vec(Pk) + vec(f) + y(32) + z(32).
|
||||
func readBGEProofBytes(dec *wire.Decoder) []byte {
|
||||
|
|
|
|||
|
|
@ -163,3 +163,57 @@ func TestVerifyV2Signatures_TxHashMixin10(t *testing.T) {
|
|||
expectedHash := "87fbc60cde013579e1ad6ab403dee81c4da7a6b4621bea44f6973568c37b0af6"
|
||||
assert.Equal(t, expectedHash, hex.EncodeToString(txHash[:]))
|
||||
}
|
||||
|
||||
func TestParseV2ExtraMetadata_Good(t *testing.T) {
|
||||
tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex")
|
||||
|
||||
meta, err := parseV2ExtraMetadata(tx.Extra)
|
||||
require.NoError(t, err)
|
||||
require.True(t, meta.hasTxPubKey)
|
||||
require.True(t, meta.hasFee)
|
||||
assert.NotEqual(t, [32]byte{}, meta.txPublicKey)
|
||||
assert.NotZero(t, meta.fee)
|
||||
}
|
||||
|
||||
func TestVerifyBalanceProof_Good_Mixin0(t *testing.T) {
|
||||
tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex")
|
||||
|
||||
sigEntries, err := parseV2Signatures(tx.SignaturesRaw)
|
||||
require.NoError(t, err)
|
||||
|
||||
proofs, err := parseV2Proofs(tx.Proofs)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = verifyBalanceProof(tx, sigEntries, proofs, wire.TransactionPrefixHash(tx))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestVerifyBalanceProof_Bad_CorruptProof(t *testing.T) {
|
||||
tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex")
|
||||
|
||||
sigEntries, err := parseV2Signatures(tx.SignaturesRaw)
|
||||
require.NoError(t, err)
|
||||
|
||||
proofs, err := parseV2Proofs(tx.Proofs)
|
||||
require.NoError(t, err)
|
||||
proofs.balanceProof[0] ^= 0x01
|
||||
|
||||
err = verifyBalanceProof(tx, sigEntries, proofs, wire.TransactionPrefixHash(tx))
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "balance proof verification failed")
|
||||
}
|
||||
|
||||
func TestVerifyBalanceProof_Bad_MissingProof(t *testing.T) {
|
||||
tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex")
|
||||
|
||||
sigEntries, err := parseV2Signatures(tx.SignaturesRaw)
|
||||
require.NoError(t, err)
|
||||
|
||||
proofs, err := parseV2Proofs(tx.Proofs)
|
||||
require.NoError(t, err)
|
||||
proofs.balanceProof = nil
|
||||
|
||||
err = verifyBalanceProof(tx, sigEntries, proofs, wire.TransactionPrefixHash(tx))
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "zc_balance_proof is missing")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
if err := verifyBalanceProof(tx, sigEntries, proofs, prefixHash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -320,3 +321,70 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyBalanceProof(tx *types.Transaction, sigEntries []v2SigEntry,
|
||||
proofs *v2ProofData, prefixHash types.Hash) error {
|
||||
|
||||
if len(proofs.balanceProof) == 0 {
|
||||
return coreerr.E("verifyBalanceProof", "consensus: zc_balance_proof is missing in tx proofs", nil)
|
||||
}
|
||||
if len(proofs.balanceProof) != 96 {
|
||||
return coreerr.E("verifyBalanceProof", fmt.Sprintf("consensus: zc_balance_proof length %d != 96", len(proofs.balanceProof)), nil)
|
||||
}
|
||||
|
||||
extraMeta, err := parseV2ExtraMetadata(tx.Extra)
|
||||
if err != nil {
|
||||
return coreerr.E("verifyBalanceProof", "consensus", err)
|
||||
}
|
||||
if !extraMeta.hasTxPubKey {
|
||||
return coreerr.E("verifyBalanceProof", "consensus: tx public key missing from extra", nil)
|
||||
}
|
||||
if !extraMeta.hasFee && !isCoinbase(tx) {
|
||||
return coreerr.E("verifyBalanceProof", "consensus: fee missing from extra", nil)
|
||||
}
|
||||
|
||||
bareInputsSum, err := sumInputs(tx)
|
||||
if err != nil {
|
||||
return coreerr.E("verifyBalanceProof", "consensus: sum bare inputs", err)
|
||||
}
|
||||
|
||||
outputCommitments := make([][32]byte, 0, len(tx.Vout))
|
||||
for i, out := range tx.Vout {
|
||||
zcOut, ok := out.(types.TxOutputZarcanum)
|
||||
if !ok {
|
||||
return coreerr.E("verifyBalanceProof", fmt.Sprintf("consensus: output %d has unexpected type %T", i, out), nil)
|
||||
}
|
||||
outputCommitments = append(outputCommitments, [32]byte(zcOut.AmountCommitment))
|
||||
}
|
||||
|
||||
var pseudoOutCommitments [][32]byte
|
||||
zcInputsCount := 0
|
||||
for _, vin := range tx.Vin {
|
||||
if _, ok := vin.(types.TxInputZC); ok {
|
||||
zcInputsCount++
|
||||
}
|
||||
}
|
||||
for _, entry := range sigEntries {
|
||||
if entry.pseudoOutCommitment != nil {
|
||||
pseudoOutCommitments = append(pseudoOutCommitments, *entry.pseudoOutCommitment)
|
||||
}
|
||||
}
|
||||
if zcInputsCount != len(pseudoOutCommitments) {
|
||||
return coreerr.E("verifyBalanceProof", fmt.Sprintf("consensus: zc inputs count %d and zc sigs count %d mismatch", zcInputsCount, len(pseudoOutCommitments)), nil)
|
||||
}
|
||||
|
||||
if !crypto.VerifyBalanceProof(
|
||||
[32]byte(prefixHash),
|
||||
extraMeta.txPublicKey,
|
||||
proofs.balanceProof,
|
||||
outputCommitments,
|
||||
pseudoOutCommitments,
|
||||
bareInputsSum,
|
||||
extraMeta.fee,
|
||||
zcInputsCount > 0,
|
||||
) {
|
||||
return coreerr.E("verifyBalanceProof", "consensus: balance proof verification failed", nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,16 @@ 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 (len != 96) return false;
|
||||
size_t off = 0;
|
||||
if (!read_scalar(buf, len, &off, sig.c)) return false;
|
||||
if (!read_scalar(buf, len, &off, sig.y0)) return false;
|
||||
if (!read_scalar(buf, len, &off, sig.y1)) return false;
|
||||
return off == len;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
extern "C" {
|
||||
|
|
@ -639,6 +649,70 @@ int cn_bge_verify(const uint8_t context[32], const uint8_t *ring,
|
|||
}
|
||||
}
|
||||
|
||||
// ── Transaction Balance Proof (HF4+) ─────────────────────
|
||||
|
||||
int cn_balance_proof_verify(const uint8_t hash[32], const uint8_t tx_pub_key[32],
|
||||
const uint8_t proof[96],
|
||||
const uint8_t *output_commitments, size_t num_outputs,
|
||||
const uint8_t *pseudo_out_commitments, size_t num_pseudo_outs,
|
||||
uint64_t bare_inputs_sum, uint64_t fee,
|
||||
int has_zc_inputs) {
|
||||
if (hash == nullptr || tx_pub_key == nullptr || proof == nullptr ||
|
||||
output_commitments == nullptr || num_outputs == 0) {
|
||||
return 1;
|
||||
}
|
||||
if (num_pseudo_outs > 0 && pseudo_out_commitments == nullptr) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
crypto::hash tx_hash;
|
||||
memcpy(&tx_hash, hash, 32);
|
||||
|
||||
crypto::public_key tx_pub;
|
||||
memcpy(&tx_pub, tx_pub_key, 32);
|
||||
|
||||
crypto::generic_double_schnorr_sig dss;
|
||||
if (!deserialise_double_schnorr(proof, 96, dss)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
crypto::point_t outs_commitments_sum = crypto::c_point_0;
|
||||
for (size_t i = 0; i < num_outputs; i++) {
|
||||
crypto::public_key pk;
|
||||
memcpy(&pk, output_commitments + i * 32, 32);
|
||||
outs_commitments_sum += crypto::point_t(pk);
|
||||
}
|
||||
|
||||
crypto::point_t pseudo_outs_sum = crypto::c_point_0;
|
||||
for (size_t i = 0; i < num_pseudo_outs; i++) {
|
||||
crypto::public_key pk;
|
||||
memcpy(&pk, pseudo_out_commitments + i * 32, 32);
|
||||
pseudo_outs_sum += crypto::point_t(pk);
|
||||
}
|
||||
|
||||
outs_commitments_sum.modify_mul8();
|
||||
pseudo_outs_sum.modify_mul8();
|
||||
|
||||
crypto::point_t commitment_to_zero =
|
||||
(crypto::scalar_t(bare_inputs_sum) - crypto::scalar_t(fee)) * crypto::c_point_H +
|
||||
pseudo_outs_sum - outs_commitments_sum;
|
||||
|
||||
bool ok = false;
|
||||
if (has_zc_inputs != 0) {
|
||||
ok = crypto::verify_double_schnorr_sig<crypto::gt_X, crypto::gt_G>(
|
||||
tx_hash, commitment_to_zero, tx_pub, dss);
|
||||
} else {
|
||||
ok = crypto::verify_double_schnorr_sig<crypto::gt_G, crypto::gt_G>(
|
||||
tx_hash, commitment_to_zero, tx_pub, dss);
|
||||
}
|
||||
|
||||
return ok ? 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,21 @@ 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);
|
||||
|
||||
// ── Transaction Balance Proof (HF4+) ─────────────────────
|
||||
// Verifies zc_balance_proof (generic_double_schnorr_sig_s, 96 bytes).
|
||||
// output_commitments and pseudo_out_commitments are flat arrays of 32-byte
|
||||
// public keys, each stored in the on-chain 1/8-premultiplied form.
|
||||
// bare_inputs_sum and fee are expressed in atomic units.
|
||||
// has_zc_inputs selects the gt_X/gt_G proof variant:
|
||||
// 0 => verify_double_schnorr_sig<gt_G, gt_G>
|
||||
// 1 => verify_double_schnorr_sig<gt_X, gt_G>
|
||||
int cn_balance_proof_verify(const uint8_t hash[32], const uint8_t tx_pub_key[32],
|
||||
const uint8_t proof[96],
|
||||
const uint8_t *output_commitments, size_t num_outputs,
|
||||
const uint8_t *pseudo_out_commitments, size_t num_pseudo_outs,
|
||||
uint64_t bare_inputs_sum, uint64_t fee,
|
||||
int has_zc_inputs);
|
||||
|
||||
// ── Zarcanum PoS ──────────────────────────────────────────
|
||||
// TODO: extend API to accept kernel_hash, ring, last_pow_block_id,
|
||||
// stake_ki, pos_difficulty. Currently returns -1 (not implemented).
|
||||
|
|
|
|||
|
|
@ -74,6 +74,53 @@ func VerifyBGE(context [32]byte, ring [][32]byte, proof []byte) bool {
|
|||
) == 0
|
||||
}
|
||||
|
||||
// VerifyBalanceProof verifies a post-HF4 zc_balance_proof.
|
||||
// outputCommitments and pseudoOutCommitments must use the on-chain 1/8-premultiplied form.
|
||||
func VerifyBalanceProof(hash [32]byte, txPubKey [32]byte, proof []byte,
|
||||
outputCommitments [][32]byte, pseudoOutCommitments [][32]byte,
|
||||
bareInputsSum uint64, fee uint64, hasZCInputs bool) bool {
|
||||
|
||||
if len(proof) != 96 || len(outputCommitments) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
flatOutputs := make([]byte, len(outputCommitments)*32)
|
||||
for i, c := range outputCommitments {
|
||||
copy(flatOutputs[i*32:], c[:])
|
||||
}
|
||||
|
||||
var flatPseudoOuts []byte
|
||||
if len(pseudoOutCommitments) > 0 {
|
||||
flatPseudoOuts = make([]byte, len(pseudoOutCommitments)*32)
|
||||
for i, c := range pseudoOutCommitments {
|
||||
copy(flatPseudoOuts[i*32:], c[:])
|
||||
}
|
||||
}
|
||||
|
||||
hasZCInputsFlag := 0
|
||||
if hasZCInputs {
|
||||
hasZCInputsFlag = 1
|
||||
}
|
||||
|
||||
var pseudoPtr *C.uint8_t
|
||||
if len(flatPseudoOuts) > 0 {
|
||||
pseudoPtr = (*C.uint8_t)(unsafe.Pointer(&flatPseudoOuts[0]))
|
||||
}
|
||||
|
||||
return C.cn_balance_proof_verify(
|
||||
(*C.uint8_t)(unsafe.Pointer(&hash[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&txPubKey[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&proof[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&flatOutputs[0])),
|
||||
C.size_t(len(outputCommitments)),
|
||||
pseudoPtr,
|
||||
C.size_t(len(pseudoOutCommitments)),
|
||||
C.uint64_t(bareInputsSum),
|
||||
C.uint64_t(fee),
|
||||
C.int(hasZCInputsFlag),
|
||||
) == 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