// 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 ( "fmt" coreerr "dappco.re/go/core/log" "dappco.re/go/core/blockchain/config" "dappco.re/go/core/blockchain/crypto" "dappco.re/go/core/blockchain/types" "dappco.re/go/core/blockchain/wire" ) // RingOutputsFn fetches the public keys for a ring at the given spending // height, amount, and offsets. Used to decouple consensus/ from chain storage. type RingOutputsFn func(height, amount uint64, offsets []uint64) ([]types.PublicKey, error) // ZCRingMember holds the three public keys per ring entry needed for // CLSAG GGX verification (HF4+). All fields are premultiplied by 1/8 // as stored on chain. type ZCRingMember struct { StealthAddress [32]byte AmountCommitment [32]byte BlindedAssetID [32]byte } // ZCRingOutputsFn fetches ZC ring members for the given global output indices. // Used for post-HF4 CLSAG GGX signature verification. type ZCRingOutputsFn func(offsets []uint64) ([]ZCRingMember, error) // VerifyTransactionSignatures verifies all ring signatures in a transaction. // For coinbase transactions, this is a no-op (no signatures). // For pre-HF4 transactions, NLSAG ring signatures are verified. // For post-HF4, CLSAG signatures and proofs are verified. // // getRingOutputs is used for pre-HF4 (V1) signature verification. // getZCRingOutputs is used for post-HF4 (V2) CLSAG GGX verification. // Either may be nil for structural-only checks. // // consensus.VerifyTransactionSignatures(&tx, config.MainnetForks, height, chain.GetRingOutputs, chain.GetZCRingOutputs) // consensus.VerifyTransactionSignatures(&tx, config.MainnetForks, height, nil, nil) // structural only func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork, height uint64, getRingOutputs RingOutputsFn, getZCRingOutputs ZCRingOutputsFn) error { // Coinbase: no signatures. if isCoinbase(tx) { return nil } hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height) if !hardForkFourActive { return verifyV1Signatures(tx, height, getRingOutputs) } return verifyV2Signatures(tx, getZCRingOutputs) } // verifyV1Signatures checks NLSAG ring signatures for pre-HF4 transactions. func verifyV1Signatures(tx *types.Transaction, height uint64, getRingOutputs RingOutputsFn) error { // Count ring-signing inputs (TxInputToKey and TxInputHTLC contribute // ring signatures; TxInputMultisig does not). var ringInputCount int for _, vin := range tx.Vin { switch vin.(type) { case types.TxInputToKey, types.TxInputHTLC: ringInputCount++ } } if len(tx.Signatures) != ringInputCount { return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: signature count %d != input count %d", len(tx.Signatures), ringInputCount), nil) } // Actual NLSAG verification requires the crypto bridge and ring outputs. // When getRingOutputs is nil, we can only check structural correctness. if getRingOutputs == nil { return nil } prefixHash := wire.TransactionPrefixHash(tx) var sigIdx int for _, vin := range tx.Vin { // Extract amount and key offsets from ring-signing input types. var amount uint64 var keyOffsets []types.TxOutRef var keyImage types.KeyImage switch v := vin.(type) { case types.TxInputToKey: amount = v.Amount keyOffsets = v.KeyOffsets keyImage = v.KeyImage case types.TxInputHTLC: amount = v.Amount keyOffsets = v.KeyOffsets keyImage = v.KeyImage default: continue // TxInputMultisig and others do not use NLSAG } // Extract absolute global indices from key offsets. offsets := make([]uint64, len(keyOffsets)) for i, ref := range keyOffsets { offsets[i] = ref.GlobalIndex } ringKeys, err := getRingOutputs(height, amount, offsets) if err != nil { return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: failed to fetch ring outputs for input %d", sigIdx), err) } ringSigs := tx.Signatures[sigIdx] if len(ringSigs) != len(ringKeys) { return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: input %d has %d signatures but ring size %d", sigIdx, len(ringSigs), len(ringKeys)), nil) } // Convert typed slices to raw byte arrays for the crypto bridge. pubs := make([][32]byte, len(ringKeys)) for i, pk := range ringKeys { pubs[i] = [32]byte(pk) } sigs := make([][64]byte, len(ringSigs)) for i, s := range ringSigs { sigs[i] = [64]byte(s) } if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(keyImage), pubs, sigs) { return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: ring signature verification failed for input %d", sigIdx), nil) } sigIdx++ } return nil } // verifyV2Signatures checks CLSAG signatures and proofs for post-HF4 transactions. func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn) error { // Parse the signature variant vector. sigEntries, err := parseV2Signatures(tx.SignaturesRaw) if err != nil { return coreerr.E("verifyV2Signatures", "consensus", err) } // Match signatures to inputs: each input must have a corresponding signature. if len(sigEntries) != len(tx.Vin) { return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: V2 signature count %d != input count %d", len(sigEntries), len(tx.Vin)), nil) } // Validate that ZC inputs have ZC_sig and that ring-spending inputs use // the ring-signature tags that match their spending model. for i, vin := range tx.Vin { switch vin.(type) { case types.TxInputZC: if sigEntries[i].tag != types.SigTypeZC { return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is ZC but signature tag is 0x%02x", i, sigEntries[i].tag), nil) } case types.TxInputToKey: if sigEntries[i].tag != types.SigTypeNLSAG && sigEntries[i].tag != types.SigTypeVoid { return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is to_key but signature tag is 0x%02x", i, sigEntries[i].tag), nil) } case types.TxInputHTLC: if sigEntries[i].tag != types.SigTypeNLSAG && sigEntries[i].tag != types.SigTypeVoid { return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is HTLC but signature tag is 0x%02x", i, sigEntries[i].tag), nil) } } } // Without ring output data, we can only check structural correctness. if getZCRingOutputs == nil { return nil } // Compute tx prefix hash for CLSAG verification. // For normal transactions (not TX_FLAG_SIGNATURE_MODE_SEPARATE), // the hash is simply the transaction prefix hash. prefixHash := wire.TransactionPrefixHash(tx) // Verify CLSAG GGX signature for each ZC input. for i, vin := range tx.Vin { zcIn, ok := vin.(types.TxInputZC) if !ok { continue } zc := sigEntries[i].zcSig if zc == nil { return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d: missing ZC_sig data", i), nil) } // Extract absolute global indices from key offsets. offsets := make([]uint64, len(zcIn.KeyOffsets)) for j, ref := range zcIn.KeyOffsets { offsets[j] = ref.GlobalIndex } ringMembers, err := getZCRingOutputs(offsets) if err != nil { return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: failed to fetch ZC ring outputs for input %d", i), err) } if len(ringMembers) != zc.ringSize { return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d: ring size %d from chain != %d from sig", i, len(ringMembers), zc.ringSize), nil) } // Build flat ring: [stealth(32) | commitment(32) | blinded_asset_id(32)] per entry. ring := make([]byte, 0, len(ringMembers)*96) for _, m := range ringMembers { ring = append(ring, m.StealthAddress[:]...) ring = append(ring, m.AmountCommitment[:]...) ring = append(ring, m.BlindedAssetID[:]...) } if !crypto.VerifyCLSAGGGX( [32]byte(prefixHash), ring, zc.ringSize, zc.pseudoOutCommitment, zc.pseudoOutAssetID, [32]byte(zcIn.KeyImage), zc.clsagFlatSig, ) { return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: CLSAG GGX verification failed for input %d", i), nil) } } // Parse and verify proofs. proofs, err := parseV2Proofs(tx.Proofs) if err != nil { return coreerr.E("verifyV2Signatures", "consensus", err) } // Verify BPP range proof if present. if len(proofs.bppProofBytes) > 0 && len(proofs.bppCommitments) > 0 { if !crypto.VerifyBPP(proofs.bppProofBytes, proofs.bppCommitments) { return coreerr.E("verifyV2Signatures", "consensus: BPP range proof verification failed", nil) } } // Verify BGE asset surjection proofs. // One proof per output, with a ring of (pseudo_out_asset_id - output_asset_id) // per ZC input. if len(proofs.bgeProofs) > 0 { if err := verifyBGEProofs(tx, sigEntries, proofs, prefixHash); err != nil { return err } } // Balance proofs are verified by the generic double-Schnorr helper in // consensus.VerifyBalanceProof once the transaction-specific public // points have been constructed. return nil } // verifyBGEProofs verifies the BGE asset surjection proofs. // There is one BGE proof per Zarcanum output. For each output j, the proof // demonstrates that the output's blinded asset ID matches one of the // pseudo-out asset IDs from the ZC inputs. // // The BGE ring for output j has one entry per ZC input i: // // ring[i] = mul8(pseudo_out_blinded_asset_id_i) - mul8(output_blinded_asset_id_j) // // The context hash is the transaction prefix hash (tx_id). func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry, proofs *v2ProofData, prefixHash types.Hash) error { // Collect Zarcanum output blinded asset IDs. var outputAssetIDs [][32]byte for _, out := range tx.Vout { if zOut, ok := out.(types.TxOutputZarcanum); ok { outputAssetIDs = append(outputAssetIDs, [32]byte(zOut.BlindedAssetID)) } } if len(proofs.bgeProofs) != len(outputAssetIDs) { return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE proof count %d != Zarcanum output count %d", len(proofs.bgeProofs), len(outputAssetIDs)), nil) } // Collect pseudo-out asset IDs from ZC signatures and expand to full points. var pseudoOutAssetIDs [][32]byte for _, entry := range sigEntries { if entry.tag == types.SigTypeZC && entry.zcSig != nil { pseudoOutAssetIDs = append(pseudoOutAssetIDs, entry.zcSig.pseudoOutAssetID) } } // mul8 all pseudo-out asset IDs (stored premultiplied by 1/8 on chain). mul8PseudoOuts := make([][32]byte, len(pseudoOutAssetIDs)) for i, p := range pseudoOutAssetIDs { full, err := crypto.PointMul8(p) if err != nil { return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: mul8 pseudo-out asset ID %d", i), err) } mul8PseudoOuts[i] = full } // For each output, build the BGE ring and verify. context := [32]byte(prefixHash) for j, outAssetID := range outputAssetIDs { // mul8 the output's blinded asset ID. mul8Out, err := crypto.PointMul8(outAssetID) if err != nil { return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: mul8 output asset ID %d", j), err) } // ring[i] = mul8(pseudo_out_i) - mul8(output_j) ring := make([][32]byte, len(mul8PseudoOuts)) for i, mul8Pseudo := range mul8PseudoOuts { diff, err := crypto.PointSub(mul8Pseudo, mul8Out) if err != nil { return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE ring[%d][%d] sub", j, i), err) } ring[i] = diff } if !crypto.VerifyBGE(context, ring, proofs.bgeProofs[j]) { return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE proof verification failed for output %d", j), nil) } } return nil }