diff --git a/conan.lock b/conan.lock index a1e1c3647c..f141ac43f2 100644 --- a/conan.lock +++ b/conan.lock @@ -12,7 +12,7 @@ "protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038", "openssl/3.5.5#05a4ac5b7323f7a329b2db1391d9941f%1770229825.601", "nudb/2.0.9#0432758a24204da08fee953ec9ea03cb%1769436073.32", - "mpt-crypto/0.1.0-rc2#575de3d495f539e3e5eba957b324d260%1771955268.105", + "mpt-crypto/0.2.0-rc1#ed3f241f69d8b9ebf80069d1923d93a8%1773853481.755", "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914", "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03", diff --git a/conanfile.py b/conanfile.py index fa178f08d9..e459ef5201 100644 --- a/conanfile.py +++ b/conanfile.py @@ -31,7 +31,7 @@ class Xrpl(ConanFile): "ed25519/2015.03", "grpc/1.72.0", "libarchive/3.8.1", - "mpt-crypto/0.1.0-rc2", + "mpt-crypto/0.2.0-rc1", "nudb/2.0.9", "openssl/3.5.5", "secp256k1/0.7.1", diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index 34378ec418..7ebfbace16 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -56,27 +56,6 @@ incrementConfidentialVersion(STObject& mptoken) mptoken[~sfConfidentialBalanceVersion].value_or(0u) + 1u; } -/** - * @brief Adds common fields to a serializer for ZKP context hash generation. - * - * Serializes the transaction type, account, issuance ID and sequence/ticket number - * into the provided serializer. These fields form the base of all context - * hashes used in zero-knowledge proofs. - * - * @param s The serializer to append fields to. - * @param txType The transaction type identifier. - * @param account The account ID of the transaction sender. - * @param issuanceID The MPToken Issuance ID. - * @param sequence The transaction sequence number or ticket number. - */ -void -addCommonZKPFields( - Serializer& s, - std::uint16_t txType, - AccountID const& account, - uint192 const& issuanceID, - std::uint32_t sequence); - /** * @brief Generates the context hash for ConfidentialMPTSend transactions. * @@ -265,25 +244,6 @@ encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, M TER verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash); -/** - * @brief Verifies that a ciphertext correctly encrypts a revealed amount. - * - * Given the plaintext amount and blinding factor, verifies that the - * ciphertext was correctly constructed using ElGamal encryption. - * - * @param amount The revealed plaintext amount. - * @param blindingFactor The blinding factor used in encryption (size=xrpl::ecBlindingFactorLength). - * @param pubKeySlice The recipient's ElGamal public key (size=xrpl::ecPubKeyLength). - * @param ciphertext The ciphertext to verify (size=xrpl::ecGamalEncryptedTotalLength). - * @return tesSUCCESS if the encryption is valid, or an error code otherwise. - */ -TER -verifyElGamalEncryption( - uint64_t const amount, - Slice const& blindingFactor, - Slice const& pubKeySlice, - Slice const& ciphertext); - /** * @brief Validates the format of encrypted amount fields in a transaction. * @@ -351,25 +311,6 @@ getEqualityProofSize(std::size_t nRecipients) return secp256k1_mpt_proof_equality_shared_r_size(nRecipients); } -/** - * @brief Verifies a multi-ciphertext equality proof. - * - * Proves that all ciphertexts in the recipients vector encrypt the same - * plaintext amount, without revealing the amount itself. - * - * @param proof The zero-knowledge proof bytes. - * @param recipients Vector of recipients with their public keys and ciphertexts. - * @param nRecipients The number of recipients (must match recipients.size()). - * @param contextHash The 256-bit context hash binding the proof. - * @return tesSUCCESS if the proof is valid, or an error code otherwise. - */ -TER -verifyMultiCiphertextEqualityProof( - Slice const& proof, - std::vector const& recipients, - std::size_t const nRecipients, - uint256 const& contextHash); - /** * @brief Verifies a clawback equality proof. * @@ -462,21 +403,63 @@ verifyAggregatedBulletproof( uint256 const& contextHash); /** - * @brief Computes the remainder commitment for ConfidentialMPTSend. + * @brief Verifies all zero-knowledge proofs for a ConfidentialMPTSend transaction. * - * Given a balance commitment PC_bal = m_bal*G + rho_bal*H and an amount - * commitment PC_amt = m_amt*G + rho_amt*H, this function computes: - * PC_rem = PC_bal - PC_amt = (m_bal - m_amt)*G + (rho_bal - rho_amt)*H + * This function calls mpt_verify_send_proof API in the mpt-crypto utility lib, which verifies the + * equality proof, amount linkage, balance linkage, and range proof. + * Equality proof: Proves the same value is encrypted for the sender, receiver, issuer, and auditor. + * Amount linkage: Proves the send amount matches the amount Pedersen commitment. + * Balance linkage: Proves the sender's balance matches the balance Pedersen + * commitment. + * Range proof: Proves the amount and the remaining balance are within range [0, 2^64-1]. * - * This derived commitment is used in an aggregated range proof to ensure - * the sender maintains a non-negative balance (m_bal - m_amt >= 0). - * - * @param balanceCommitment The compressed Pedersen commitment to the balance (33 bytes). - * @param amountCommitment The compressed Pedersen commitment to the amount (33 bytes). - * @return The remainder commitment (33 bytes), or std::nullopt on failure. + * @param proof The full proof blob. + * @param sender The sender's public key and encrypted amount. + * @param destination The destination's public key and encrypted amount. + * @param issuer The issuer's public key and encrypted amount. + * @param auditor The auditor's public key and encrypted amount if present. + * @param spendingBalance The sender's current spending balance ciphertext. + * @param amountCommitment The Pedersen commitment to the send amount. + * @param balanceCommitment The Pedersen commitment to the sender's balance. + * @param contextHash The context hash binding the proof. + * @return tesSUCCESS if all proofs are valid, or an error code otherwise. */ -std::optional -computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment); +TER +verifySendProof( + Slice const& proof, + ConfidentialRecipient const& sender, + ConfidentialRecipient const& destination, + ConfidentialRecipient const& issuer, + std::optional const& auditor, + Slice const& spendingBalance, + Slice const& amountCommitment, + Slice const& balanceCommitment, + uint256 const& contextHash); + +/** + * @brief Verifies all zero-knowledge proofs for a ConfidentialMPTConvertBack transaction. + * + * This function calls mpt_verify_convert_back_proof API in the mpt-crypto utility lib, which + * verifies the balance linkage proof and range proof. Balance linkage proof: proves the balance + * commitment matches the spending ciphertext. Range proof: proves the remaining balance after + * convert back is within range [0, 2^64-1]. + * + * @param proof The full proof blob. + * @param pubKeySlice The holder's public key. + * @param spendingBalance The holder's spending balance ciphertext. + * @param balanceCommitment The Pedersen commitment to the balance. + * @param amount The amount being converted back to public. + * @param contextHash The context hash binding the proof. + * @return tesSUCCESS if all proofs are valid, or an error code otherwise. + */ +TER +verifyConvertBackProof( + Slice const& proof, + Slice const& pubKeySlice, + Slice const& spendingBalance, + Slice const& balanceCommitment, + uint64_t amount, + uint256 const& contextHash); /** * @brief Computes the remainder commitment for ConvertBack. diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index d37f0cdf84..40430cf316 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -1,28 +1,37 @@ #include #include -#include - #include -#include +#include namespace xrpl { -static constexpr std::uint32_t defaultVersion = 0; - -void -addCommonZKPFields( - Serializer& s, - std::uint16_t txType, - AccountID const& account, - uint192 const& issuanceID, - std::uint32_t sequence) +/** + * @brief Converts an XRPL AccountID to mpt-crypto lib C struct. + * + * @param account The AccountID. + * @return The equivalent mpt-crypto lib account_id struct. + */ +account_id +toAccountId(AccountID const& account) { - // TxCommonHash = hash(TxType || Account || IssuanceID || SequenceOrTicket) - s.add16(txType); - s.addBitString(account); - s.addBitString(issuanceID); - s.add32(sequence); + account_id res; + std::memcpy(res.bytes, account.data(), kMPT_ACCOUNT_ID_SIZE); + return res; +} + +/** + * @brief Converts an XRPL uint192 to mpt-crypto lib C struct. + * + * @param i The XRPL MPTokenIssuance ID. + * @return The equivalent mpt-crypto lib mpt_issuance_id struct. + */ +mpt_issuance_id +toIssuanceId(uint192 const& issuance) +{ + mpt_issuance_id res; + std::memcpy(res.bytes, issuance.data(), kMPT_ISSUANCE_ID_SIZE); + return res; } uint256 @@ -33,14 +42,15 @@ getSendContextHash( AccountID const& destination, std::uint32_t version) { - Serializer s; - addCommonZKPFields(s, ttCONFIDENTIAL_MPT_SEND, account, issuanceID, sequence); - - // TxSpecific = identity || freshness - s.addBitString(destination); - s.addInteger(version); - - return s.getSHA512Half(); + uint256 result; + mpt_get_send_context_hash( + toAccountId(account), + toIssuanceId(issuanceID), + sequence, + toAccountId(destination), + version, + result.data()); + return result; } uint256 @@ -50,27 +60,23 @@ getClawbackContextHash( std::uint32_t sequence, AccountID const& holder) { - Serializer s; - addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CLAWBACK, account, issuanceID, sequence); - - // TxSpecific = identity || freshness - s.addBitString(holder); - s.addInteger(defaultVersion); - - return s.getSHA512Half(); + uint256 result; + mpt_get_clawback_context_hash( + toAccountId(account), + toIssuanceId(issuanceID), + sequence, + toAccountId(holder), + result.data()); + return result; } uint256 getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence) { - Serializer s; - addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT, account, issuanceID, sequence); - - // TxSpecific = identity || freshness - s.addBitString(account); - s.addInteger(defaultVersion); - - return s.getSHA512Half(); + uint256 result; + mpt_get_convert_context_hash( + toAccountId(account), toIssuanceId(issuanceID), sequence, result.data()); + return result; } uint256 @@ -80,14 +86,10 @@ getConvertBackContextHash( std::uint32_t sequence, std::uint32_t version) { - Serializer s; - addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT_BACK, account, issuanceID, sequence); - - // TxSpecific = identity || freshness - s.addBitString(account); - s.addInteger(version); - - return s.getSHA512Half(); + uint256 result; + mpt_get_convert_back_context_hash( + toAccountId(account), toIssuanceId(issuanceID), sequence, version, result.data()); + return result; } std::optional @@ -216,29 +218,14 @@ generateBlindingFactor() std::optional encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor) { - if (blindingFactor.size() != ecBlindingFactorLength) + if (blindingFactor.size() != ecBlindingFactorLength || pubKeySlice.size() != ecPubKeyLength) return std::nullopt; - if (pubKeySlice.size() != ecPubKeyLength) + Buffer out(ecGamalEncryptedTotalLength); + if (mpt_encrypt_amount(amt, pubKeySlice.data(), blindingFactor.data(), out.data()) != 0) return std::nullopt; - EcPair pair; - secp256k1_pubkey pubKey; - if (auto res = secp256k1_ec_pubkey_parse( - secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength); - res != 1) - { - return std::nullopt; - } - - if (auto res = secp256k1_elgamal_encrypt( - secp256k1Context(), &pair.c1, &pair.c2, &pubKey, amt, blindingFactor.data()); - res != 1) - { - return std::nullopt; - } - - return serializeEcPair(pair); + return out; } std::optional @@ -266,66 +253,6 @@ encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, M return serializeEcPair(pair); } -TER -verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash) -{ - if (proofSlice.size() != ecSchnorrProofLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (pubKeySlice.size() != ecPubKeyLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - secp256k1_pubkey pubKey; - if (auto res = secp256k1_ec_pubkey_parse( - secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - if (auto res = secp256k1_mpt_pok_sk_verify( - secp256k1Context(), proofSlice.data(), &pubKey, contextHash.data()); - res != 1) - { - return tecBAD_PROOF; - } - - return tesSUCCESS; -} - -TER -verifyElGamalEncryption( - uint64_t const amount, - Slice const& blindingFactor, - Slice const& pubKeySlice, - Slice const& ciphertext) -{ - if (ciphertext.size() != ecGamalEncryptedTotalLength || - blindingFactor.size() != ecBlindingFactorLength || pubKeySlice.size() != ecPubKeyLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - secp256k1_pubkey pubKey; - if (auto res = secp256k1_ec_pubkey_parse( - secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - auto const pair = makeEcPair(ciphertext); - if (!pair) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (auto res = secp256k1_elgamal_verify_encryption( - secp256k1Context(), &pair->c1, &pair->c2, &pubKey, amount, blindingFactor.data()); - res != 1) - { - return tecBAD_PROOF; - } - - return tesSUCCESS; -} - TER verifyRevealedAmount( uint64_t const amount, @@ -334,151 +261,37 @@ verifyRevealedAmount( ConfidentialRecipient const& issuer, std::optional const& auditor) { - if (auto const res = verifyElGamalEncryption( - amount, blindingFactor, holder.publicKey, holder.encryptedAmount); - !isTesSuccess(res)) - { - return res; - } + if (blindingFactor.size() != ecBlindingFactorLength || + holder.publicKey.size() != ecPubKeyLength || + holder.encryptedAmount.size() != ecGamalEncryptedTotalLength || + issuer.publicKey.size() != ecPubKeyLength || + issuer.encryptedAmount.size() != ecGamalEncryptedTotalLength) + return tecINTERNAL; // LCOV_EXCL_LINE - if (auto const res = verifyElGamalEncryption( - amount, blindingFactor, issuer.publicKey, issuer.encryptedAmount); - !isTesSuccess(res)) - { - return res; - } + auto toParticipant = [](ConfidentialRecipient const& r) { + mpt_confidential_participant p; + std::memcpy(p.pubkey, r.publicKey.data(), kMPT_PUBKEY_SIZE); + std::memcpy(p.ciphertext, r.encryptedAmount.data(), kMPT_ELGAMAL_TOTAL_SIZE); + return p; + }; + auto const holderP = toParticipant(holder); + auto const issuerP = toParticipant(issuer); + + mpt_confidential_participant auditorP; + mpt_confidential_participant const* auditorPtr = nullptr; if (auditor) { - if (auto const res = verifyElGamalEncryption( - amount, blindingFactor, auditor->publicKey, auditor->encryptedAmount); - !isTesSuccess(res)) - { - return res; - } + if (auditor->publicKey.size() != ecPubKeyLength || + auditor->encryptedAmount.size() != ecGamalEncryptedTotalLength) + return tecINTERNAL; // LCOV_EXCL_LINE + auditorP = toParticipant(*auditor); + auditorPtr = &auditorP; } - return tesSUCCESS; -} - -TER -verifyMultiCiphertextEqualityProof( - Slice const& proof, - std::vector const& recipients, - std::size_t const nRecipients, - uint256 const& contextHash) -{ - if (recipients.size() != nRecipients) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (proof.size() != getEqualityProofSize(nRecipients)) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const ctx = secp256k1Context(); - - secp256k1_pubkey c1; - std::vector c2_vec(nRecipients); - std::vector pk_vec(nRecipients); - - for (size_t i = 0; i < nRecipients; ++i) - { - auto const& recipient = recipients[i]; - - if (recipient.encryptedAmount.size() != ecGamalEncryptedTotalLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (recipient.publicKey.size() != ecPubKeyLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - // Parse Shared C1 from the first recipient only - if (i == 0) - { - if (auto res = secp256k1_ec_pubkey_parse( - ctx, &c1, recipient.encryptedAmount.data(), ecGamalEncryptedLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - } - else - { - // All C1 bytes must be the same - if (std::memcmp( - recipient.encryptedAmount.data(), - recipients[0].encryptedAmount.data(), - ecGamalEncryptedLength) != 0) - { - return tecBAD_PROOF; - } - } - - if (auto res = secp256k1_ec_pubkey_parse( - ctx, - &c2_vec[i], - recipient.encryptedAmount.data() + ecGamalEncryptedLength, - ecGamalEncryptedLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - if (auto res = secp256k1_ec_pubkey_parse( - ctx, &pk_vec[i], recipient.publicKey.data(), ecPubKeyLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - } - - if (auto res = secp256k1_mpt_verify_equality_shared_r( - ctx, proof.data(), nRecipients, &c1, c2_vec.data(), pk_vec.data(), contextHash.data()); - res != 1) - { + if (mpt_verify_revealed_amount(amount, blindingFactor.data(), &holderP, &issuerP, auditorPtr) != + 0) return tecBAD_PROOF; - } - - return tesSUCCESS; -} - -TER -verifyClawbackEqualityProof( - uint64_t const amount, - Slice const& proof, - Slice const& pubKeySlice, - Slice const& ciphertext, - uint256 const& contextHash) -{ - if (ciphertext.size() != ecGamalEncryptedTotalLength || pubKeySlice.size() != ecPubKeyLength || - proof.size() != ecEqualityProofLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const pair = makeEcPair(ciphertext); - if (!pair) - return tecINTERNAL; // LCOV_EXCL_LINE - - secp256k1_pubkey pubKey; - if (auto res = secp256k1_ec_pubkey_parse( - secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - // Note: c2, c1 order - the proof is generated with c2 first (the encrypted - // message component) because the equality proof structure expects the - // message-containing term before the blinding term. - if (auto res = secp256k1_equality_plaintext_verify( - secp256k1Context(), - proof.data(), - &pubKey, - &pair->c2, - &pair->c1, - amount, - contextHash.data()); - res != 1) - { - return tecBAD_PROOF; - } return tesSUCCESS; } @@ -511,6 +324,37 @@ checkEncryptedAmountFormat(STObject const& object) return tesSUCCESS; } +TER +verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash) +{ + if (proofSlice.size() != ecSchnorrProofLength || pubKeySlice.size() != ecPubKeyLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (mpt_verify_convert_proof(proofSlice.data(), pubKeySlice.data(), contextHash.data()) != 0) + return tecBAD_PROOF; + + return tesSUCCESS; +} + +TER +verifyClawbackEqualityProof( + uint64_t const amount, + Slice const& proof, + Slice const& pubKeySlice, + Slice const& ciphertext, + uint256 const& contextHash) +{ + if (ciphertext.size() != ecGamalEncryptedTotalLength || pubKeySlice.size() != ecPubKeyLength || + proof.size() != ecEqualityProofLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (mpt_verify_clawback_proof( + proof.data(), amount, pubKeySlice.data(), ciphertext.data(), contextHash.data()) != 0) + return tecBAD_PROOF; + + return tesSUCCESS; +} + TER verifyPcmLinkage( PcmLinkageType type, @@ -520,65 +364,30 @@ verifyPcmLinkage( Slice const& pcmSlice, uint256 const& contextHash) { - if (proof.length() != ecPedersenProofLength) + if (proof.length() != ecPedersenProofLength || pubKeySlice.size() != ecPubKeyLength || + pcmSlice.size() != ecPedersenCommitmentLength || + encAmt.size() != ecGamalEncryptedTotalLength) return tecINTERNAL; - auto const pair = makeEcPair(encAmt); - if (!pair) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (pubKeySlice.size() != ecPubKeyLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (pcmSlice.size() != ecPedersenCommitmentLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - secp256k1_pubkey pubKey; - if (auto res = secp256k1_ec_pubkey_parse( - secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - secp256k1_pubkey pcm; - if (auto res = secp256k1_ec_pubkey_parse( - secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - // For amount linkage (randomness r): order is C1, C2, Pk, Pcm. - // For balance linkage (secret key s): order is Pk, C2, C1, Pcm - // (swaps Pk <-> C1 to accommodate the different algebraic structure). int res; if (type == PcmLinkageType::amount) { - res = secp256k1_elgamal_pedersen_link_verify( + res = mpt_verify_amount_linkage( secp256k1Context(), proof.data(), - &pair->c1, - &pair->c2, - &pubKey, - &pcm, + encAmt.data(), + pubKeySlice.data(), + pcmSlice.data(), contextHash.data()); } else { - res = secp256k1_elgamal_pedersen_link_verify( - secp256k1Context(), - proof.data(), - &pubKey, - &pair->c2, - &pair->c1, - &pcm, - contextHash.data()); + res = mpt_verify_balance_linkage( + proof.data(), encAmt.data(), pubKeySlice.data(), pcmSlice.data(), contextHash.data()); } - if (res != 1) + if (res != 0) return tecBAD_PROOF; - return tesSUCCESS; } @@ -588,8 +397,6 @@ verifyAggregatedBulletproof( std::vector const& compressedCommitments, uint256 const& contextHash) { - // 1. Validate input lengths - // This function could support any power-of-2 m, but current usage only requires m=1 or m=2 std::size_t const m = compressedCommitments.size(); if (m != 1 && m != 2) return tecINTERNAL; // LCOV_EXCL_LINE @@ -599,121 +406,106 @@ verifyAggregatedBulletproof( if (proof.size() != expectedProofLen) return tecINTERNAL; // LCOV_EXCL_LINE - // 2. Prepare Pedersen Commitments, parse from compressed format - auto const ctx = secp256k1Context(); - std::vector commitments(m); + std::vector commitmentPtrs(m); for (size_t i = 0; i < m; ++i) { - // Sanity check length if (compressedCommitments[i].size() != ecPedersenCommitmentLength) return tecINTERNAL; // LCOV_EXCL_LINE - - if (auto res = secp256k1_ec_pubkey_parse( - ctx, &commitments[i], compressedCommitments[i].data(), ecPedersenCommitmentLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } + commitmentPtrs[i] = compressedCommitments[i].data(); } - // 3. Prepare Generator Vectors (G_vec, H_vec) - // The range proof requires vectors of size 64 * m - std::size_t const n = 64 * m; - std::vector G_vec(n); - std::vector H_vec(n); - - // Retrieve deterministic generators "G" and "H" - if (auto res = - secp256k1_mpt_get_generator_vector(ctx, G_vec.data(), n, (unsigned char const*)"G", 1); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - if (auto res = - secp256k1_mpt_get_generator_vector(ctx, H_vec.data(), n, (unsigned char const*)"H", 1); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - // 4. Prepare Base Generator (pk_base / H) - secp256k1_pubkey pk_base; - if (auto res = secp256k1_mpt_get_h_generator(ctx, &pk_base); res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - // 5. Verify the Proof - if (auto res = secp256k1_bulletproof_verify_agg( - ctx, - G_vec.data(), - H_vec.data(), - reinterpret_cast(proof.data()), - proof.size(), - commitments.data(), - m, - &pk_base, - contextHash.data()); - res != 1) - { + if (mpt_verify_aggregated_bulletproof( + proof.data(), proof.size(), commitmentPtrs.data(), m, contextHash.data()) != 0) return tecBAD_PROOF; - } return tesSUCCESS; } -std::optional -computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment) +TER +verifySendProof( + Slice const& proof, + ConfidentialRecipient const& sender, + ConfidentialRecipient const& destination, + ConfidentialRecipient const& issuer, + std::optional const& auditor, + Slice const& spendingBalance, + Slice const& amountCommitment, + Slice const& balanceCommitment, + uint256 const& contextHash) { - if (balanceCommitment.size() != ecPedersenCommitmentLength || - amountCommitment.size() != ecPedersenCommitmentLength) - return std::nullopt; + auto const recipientCount = getConfidentialRecipientCount(auditor.has_value()); + auto const expectedProofSize = getEqualityProofSize(recipientCount) + + 2 * ecPedersenProofLength + ecDoubleBulletproofLength; - auto const ctx = secp256k1Context(); + if (proof.size() != expectedProofSize || sender.publicKey.size() != ecPubKeyLength || + sender.encryptedAmount.size() != ecGamalEncryptedTotalLength || + destination.publicKey.size() != ecPubKeyLength || + destination.encryptedAmount.size() != ecGamalEncryptedTotalLength || + issuer.publicKey.size() != ecPubKeyLength || + issuer.encryptedAmount.size() != ecGamalEncryptedTotalLength || + spendingBalance.size() != ecGamalEncryptedTotalLength || + amountCommitment.size() != ecPedersenCommitmentLength || + balanceCommitment.size() != ecPedersenCommitmentLength) + return tecINTERNAL; // LCOV_EXCL_LINE - secp256k1_pubkey pcBalance; - if (auto res = secp256k1_ec_pubkey_parse( - ctx, &pcBalance, balanceCommitment.data(), ecPedersenCommitmentLength); - res != 1) + auto makeParticipant = [](ConfidentialRecipient const& r) { + mpt_confidential_participant p; + std::memcpy(p.pubkey, r.publicKey.data(), kMPT_PUBKEY_SIZE); + std::memcpy(p.ciphertext, r.encryptedAmount.data(), kMPT_ELGAMAL_TOTAL_SIZE); + return p; + }; + + std::vector participants(recipientCount); + participants[0] = makeParticipant(sender); + participants[1] = makeParticipant(destination); + participants[2] = makeParticipant(issuer); + if (auditor) { - return std::nullopt; + if (auditor->publicKey.size() != ecPubKeyLength || + auditor->encryptedAmount.size() != ecGamalEncryptedTotalLength) + return tecINTERNAL; + participants[3] = makeParticipant(*auditor); } - secp256k1_pubkey pcAmount; - if (auto res = secp256k1_ec_pubkey_parse( - ctx, &pcAmount, amountCommitment.data(), ecPedersenCommitmentLength); - res != 1) - { - return std::nullopt; - } + if (mpt_verify_send_proof( + proof.data(), + proof.size(), + participants.data(), + static_cast(recipientCount), + spendingBalance.data(), + amountCommitment.data(), + balanceCommitment.data(), + contextHash.data()) != 0) + return tecBAD_PROOF; - // Negate PC_amount point to get -PC_amount - if (auto res = secp256k1_ec_pubkey_negate(ctx, &pcAmount); res != 1) - { - return std::nullopt; - } + return tesSUCCESS; +} - // Compute pcRem = pcBalance + (-pcAmount) - secp256k1_pubkey const* summands[2] = {&pcBalance, &pcAmount}; - secp256k1_pubkey pcRem; - if (auto res = secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2); res != 1) - { - return std::nullopt; - } +TER +verifyConvertBackProof( + Slice const& proof, + Slice const& pubKeySlice, + Slice const& spendingBalance, + Slice const& balanceCommitment, + uint64_t amount, + uint256 const& contextHash) +{ + if (proof.size() != ecPedersenProofLength + ecSingleBulletproofLength || + pubKeySlice.size() != ecPubKeyLength || + spendingBalance.size() != ecGamalEncryptedTotalLength || + balanceCommitment.size() != ecPedersenCommitmentLength) + return tecINTERNAL; // LCOV_EXCL_LINE - // Serialize result to compressed format - Buffer out; - out.alloc(ecPedersenCommitmentLength); - size_t outLen = ecPedersenCommitmentLength; - if (auto res = secp256k1_ec_pubkey_serialize( - ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED); - res != 1 || outLen != ecPedersenCommitmentLength) - { - return std::nullopt; - } + if (mpt_verify_convert_back_proof( + proof.data(), + pubKeySlice.data(), + spendingBalance.data(), + balanceCommitment.data(), + amount, + contextHash.data()) != 0) + return tecBAD_PROOF; - return out; + return tesSUCCESS; } std::optional @@ -722,54 +514,12 @@ computeConvertBackRemainder(Slice const& commitment, uint64_t amount) if (commitment.size() != ecPedersenCommitmentLength || amount == 0) return std::nullopt; // LCOV_EXCL_LINE - auto const ctx = secp256k1Context(); - - // Parse commitment from compressed format - secp256k1_pubkey pcBalance; - if (auto res = secp256k1_ec_pubkey_parse( - ctx, &pcBalance, commitment.data(), ecPedersenCommitmentLength); - res != 1) - { - return std::nullopt; // LCOV_EXCL_LINE - } - - // Convert amount to 32-byte big-endian scalar - unsigned char mScalar[32] = {0}; - uint64_t amountBigEndian = boost::endian::native_to_big(amount); - std::memcpy(&mScalar[24], &amountBigEndian, sizeof(amountBigEndian)); - - // Compute mG = amount * G - secp256k1_pubkey mG; - if (auto res = secp256k1_ec_pubkey_create(ctx, &mG, mScalar); res != 1) - { - return std::nullopt; // LCOV_EXCL_LINE - } - - // Negate mG to get -mG - if (auto res = secp256k1_ec_pubkey_negate(ctx, &mG); res != 1) - { - return std::nullopt; // LCOV_EXCL_LINE - } - - // Compute pcRem = pcBalance + (-mG) - secp256k1_pubkey const* summands[2] = {&pcBalance, &mG}; - secp256k1_pubkey pcRem; - if (auto res = secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2); res != 1) - { - return std::nullopt; // LCOV_EXCL_LINE - } - - // Serialize result to compressed format Buffer out; out.alloc(ecPedersenCommitmentLength); - size_t outLen = ecPedersenCommitmentLength; - if (auto res = secp256k1_ec_pubkey_serialize( - ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED); - res != 1 || outLen != ecPedersenCommitmentLength) - { + if (mpt_compute_convert_back_remainder(commitment.data(), amount, out.data()) != 0) return std::nullopt; // LCOV_EXCL_LINE - } return out; } + } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp index 5cf5a84607..cf3fcea801 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp @@ -125,6 +125,10 @@ ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx) if (hasHolderKeyOnLedger && hasHolderKeyInTx) return tecDUPLICATE; + // Run all verifications before returning any error to prevent timing attacks + // that could reveal which proof failed. + bool valid = true; + Slice holderPubKey; if (hasHolderKeyInTx) { @@ -133,11 +137,10 @@ ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx) auto const contextHash = getConvertContextHash(account, issuanceID, ctx.tx.getSeqProxy().value()); - // when register new pk, verify through schnorr proof if (auto const ter = verifySchnorrProof(holderPubKey, ctx.tx[sfZKProof], contextHash); !isTesSuccess(ter)) { - return ter; + valid = false; } } else @@ -154,12 +157,21 @@ ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx) } auto const blindingFactor = ctx.tx[sfBlindingFactor]; - return verifyRevealedAmount( - amount, - Slice(blindingFactor.data(), blindingFactor.size()), - {holderPubKey, ctx.tx[sfHolderEncryptedAmount]}, - {(*sleIssuance)[sfIssuerEncryptionKey], ctx.tx[sfIssuerEncryptedAmount]}, - auditor); + if (auto const ter = verifyRevealedAmount( + amount, + Slice(blindingFactor.data(), blindingFactor.size()), + {holderPubKey, ctx.tx[sfHolderEncryptedAmount]}, + {(*sleIssuance)[sfIssuerEncryptionKey], ctx.tx[sfIssuerEncryptedAmount]}, + auditor); + !isTesSuccess(ter)) + { + valid = false; + } + + if (!valid) + return tecBAD_PROOF; + + return tesSUCCESS; } TER diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp index 825bcc1db1..6de8a8679b 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp @@ -87,7 +87,6 @@ verifyProofs( // that could reveal which proof failed. bool valid = true; - // verify revealed amount if (auto const ter = verifyRevealedAmount( amount, Slice(blindingFactor.data(), blindingFactor.size()), @@ -99,49 +98,18 @@ verifyProofs( valid = false; } - // Extract proof components - ProofReader reader(tx[sfZKProof]); - - auto const pedersenProof = reader.read(ecPedersenProofLength); - auto const bulletproof = reader.read(ecSingleBulletproofLength); - - if (!pedersenProof || !bulletproof || !reader.done()) - return tecINTERNAL; // LCOV_EXCL_LINE - - // verify el gamal pedersen linkage - if (auto const ter = verifyPcmLinkage( - PcmLinkageType::balance, - *pedersenProof, - (*mptoken)[sfConfidentialBalanceSpending], + if (auto const ter = verifyConvertBackProof( + tx[sfZKProof], holderPubKey, + (*mptoken)[sfConfidentialBalanceSpending], tx[sfBalanceCommitment], + amount, contextHash); !isTesSuccess(ter)) { valid = false; } - // verify bullet proof - { - // Compute PC_rem = PC_balance - mG (the commitment to the remaining balance) - if (auto pcRem = computeConvertBackRemainder(tx[sfBalanceCommitment], amount)) - { - // The bulletproof verifies that the remaining balance is non-negative - std::vector commitments{Slice(pcRem->data(), pcRem->size())}; - - if (auto const ter = - verifyAggregatedBulletproof(*bulletproof, commitments, contextHash); - !isTesSuccess(ter)) - { - valid = false; - } - } - else - { - valid = false; - } - } - if (!valid) return tecBAD_PROOF; diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp index 4fe47947b0..a5a68d1862 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -79,37 +79,13 @@ verifySendProofs( return tecINTERNAL; // LCOV_EXCL_LINE auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); - auto const recipientCount = getConfidentialRecipientCount(hasAuditor); - - // Extract proof components - ProofReader reader(ctx.tx[sfZKProof]); - - auto const equalityProof = reader.read(getEqualityProofSize(recipientCount)); - auto const amountLinkageProof = reader.read(ecPedersenProofLength); - auto const balanceLinkageProof = reader.read(ecPedersenProofLength); - auto const rangeProof = reader.read(ecDoubleBulletproofLength); - - if (!equalityProof || !amountLinkageProof || !balanceLinkageProof || !rangeProof || - !reader.done()) - return tecINTERNAL; // LCOV_EXCL_LINE - - // Prepare recipient list - std::vector recipients; - recipients.reserve(recipientCount); - - recipients.push_back( - {(*sleSenderMPToken)[sfHolderEncryptionKey], ctx.tx[sfSenderEncryptedAmount]}); - recipients.push_back( - {(*sleDestinationMPToken)[sfHolderEncryptionKey], ctx.tx[sfDestinationEncryptedAmount]}); - recipients.push_back({(*sleIssuance)[sfIssuerEncryptionKey], ctx.tx[sfIssuerEncryptedAmount]}); + std::optional auditor; if (hasAuditor) - { - recipients.push_back( - {(*sleIssuance)[sfAuditorEncryptionKey], ctx.tx[sfAuditorEncryptedAmount]}); - } + auditor.emplace( + ConfidentialRecipient{ + (*sleIssuance)[sfAuditorEncryptionKey], ctx.tx[sfAuditorEncryptedAmount]}); - // Prepare the context hash auto const contextHash = getSendContextHash( ctx.tx[sfAccount], ctx.tx[sfMPTokenIssuanceID], @@ -117,75 +93,16 @@ verifySendProofs( ctx.tx[sfDestination], (*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0)); - // Use a boolean flag to track validity instead of returning early on failure to prevent leaking - // information about which proof failed through timing differences - bool valid = true; - - // Verify the multi-ciphertext equality proof - if (auto const ter = verifyMultiCiphertextEqualityProof( - *equalityProof, recipients, recipientCount, contextHash); - !isTesSuccess(ter)) - { - valid = false; - } - - // Verify amount linkage - if (auto const ter = verifyPcmLinkage( - PcmLinkageType::amount, - *amountLinkageProof, - ctx.tx[sfSenderEncryptedAmount], - (*sleSenderMPToken)[sfHolderEncryptionKey], - ctx.tx[sfAmountCommitment], - contextHash); - !isTesSuccess(ter)) - { - valid = false; - } - - // Verify balance linkage - if (auto const ter = verifyPcmLinkage( - PcmLinkageType::balance, - *balanceLinkageProof, - (*sleSenderMPToken)[sfConfidentialBalanceSpending], - (*sleSenderMPToken)[sfHolderEncryptionKey], - ctx.tx[sfBalanceCommitment], - contextHash); - !isTesSuccess(ter)) - { - valid = false; - } - - // Verify Range Proof - { - // Derive PC_rem = PC_balance - PC_amount - if (auto pcRem = - computeSendRemainder(ctx.tx[sfBalanceCommitment], ctx.tx[sfAmountCommitment])) - { - // Aggregated commitments: [PC_amount, PC_rem] - // Prove that both the transfer amount and the remaining balance are in range - std::vector commitments; - commitments.push_back(ctx.tx[sfAmountCommitment]); - commitments.push_back(Slice{pcRem->data(), pcRem->size()}); - - if (auto const ter = verifyAggregatedBulletproof(*rangeProof, commitments, contextHash); - !isTesSuccess(ter)) - { - valid = false; - } - } - else - { - valid = false; - } - } - - if (!valid) - { - JLOG(ctx.j.trace()) << "ConfidentialMPTSend: One or more cryptographic proofs failed."; - return tecBAD_PROOF; - } - - return tesSUCCESS; + return verifySendProof( + ctx.tx[sfZKProof], + {(*sleSenderMPToken)[sfHolderEncryptionKey], ctx.tx[sfSenderEncryptedAmount]}, + {(*sleDestinationMPToken)[sfHolderEncryptionKey], ctx.tx[sfDestinationEncryptedAmount]}, + {(*sleIssuance)[sfIssuerEncryptionKey], ctx.tx[sfIssuerEncryptedAmount]}, + auditor, + (*sleSenderMPToken)[sfConfidentialBalanceSpending], + ctx.tx[sfAmountCommitment], + ctx.tx[sfBalanceCommitment], + contextHash); } TER