Integrate mpt-crypto SDK lib for on-chain verification (#6679)

This commit is contained in:
yinyiqian1
2026-03-27 16:26:37 -04:00
committed by GitHub
parent b34ecc476a
commit 2472ef7df7
7 changed files with 295 additions and 665 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<ConfidentialRecipient> 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<Buffer>
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment);
TER
verifySendProof(
Slice const& proof,
ConfidentialRecipient const& sender,
ConfidentialRecipient const& destination,
ConfidentialRecipient const& issuer,
std::optional<ConfidentialRecipient> 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.

View File

@@ -1,28 +1,37 @@
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Protocol.h>
#include <boost/endian/conversion.hpp>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <utility/mpt_utility.h>
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<EcPair>
@@ -216,29 +218,14 @@ generateBlindingFactor()
std::optional<Buffer>
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<Buffer>
@@ -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<ConfidentialRecipient> 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<ConfidentialRecipient> 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<secp256k1_pubkey> c2_vec(nRecipients);
std::vector<secp256k1_pubkey> 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<Slice> 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<secp256k1_pubkey> commitments(m);
std::vector<uint8_t const*> 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<secp256k1_pubkey> G_vec(n);
std::vector<secp256k1_pubkey> 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<unsigned char const*>(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<Buffer>
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment)
TER
verifySendProof(
Slice const& proof,
ConfidentialRecipient const& sender,
ConfidentialRecipient const& destination,
ConfidentialRecipient const& issuer,
std::optional<ConfidentialRecipient> 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<mpt_confidential_participant> 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<uint8_t>(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<Buffer>
@@ -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

View File

@@ -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

View File

@@ -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<Slice> 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;

View File

@@ -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<ConfidentialRecipient> 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<ConfidentialRecipient> 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<Slice> 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