Support compact AND-composed sigma proof (#6859)

This commit is contained in:
yinyiqian1
2026-04-16 17:51:31 -04:00
committed by GitHub
parent 5229ff5a45
commit 09778f2fec
12 changed files with 180 additions and 428 deletions

View File

@@ -12,7 +12,7 @@
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
"openssl/3.6.1#e6399de266349245a4542fc5f6c71552%1774458290.139",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1774883011.384",
"mpt-crypto/0.2.0-rc1#ed3f241f69d8b9ebf80069d1923d93a8%1773853481.755",
"mpt-crypto/0.3.0-rc1#468344c6855d4aeaa8bd31fb2c403f89%1776358155.918",
"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.78.1",
"libarchive/3.8.1",
"mpt-crypto/0.2.0-rc1",
"mpt-crypto/0.3.0-rc1",
"nudb/2.0.9",
"openssl/3.6.1",
"secp256k1/0.7.1",

View File

@@ -296,37 +296,22 @@ getConfidentialRecipientCount(bool hasAuditor)
}
/**
* @brief Returns the size of a multi-ciphertext equality proof.
*
* Computes the byte size required for a zero-knowledge proof that demonstrates
* multiple ciphertexts encrypt the same plaintext value. The size depends on
* the number of recipients.
*
* @param nRecipients The number of recipients (typically 3 or 4).
* @return The proof size in bytes.
*/
inline std::size_t
getEqualityProofSize(std::size_t nRecipients)
{
return secp256k1_mpt_proof_equality_shared_r_size(nRecipients);
}
/**
* @brief Verifies a clawback equality proof.
* @brief Verifies a compact sigma clawback proof.
*
* Proves that the issuer knows the exact amount encrypted in the holder's
* balance ciphertext. Used in ConfidentialMPTClawback to verify the issuer
* can decrypt the balance using their private key.
*
* @param amount The revealed plaintext amount.
* @param proof The zero-knowledge proof bytes.
* @param pubKeySlice The issuer's ElGamal public key (64 bytes).
* @param ciphertext The issuer's encrypted balance on the holder's account (66 bytes).
* @param proof The zero-knowledge proof bytes (ecClawbackProofLength).
* @param pubKeySlice The issuer's ElGamal public key (ecPubKeyLength bytes).
* @param ciphertext The issuer's encrypted balance on the holder's account
* (ecGamalEncryptedTotalLength bytes).
* @param contextHash The 256-bit context hash binding the proof.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyClawbackEqualityProof(
verifyClawbackProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
@@ -403,58 +388,4 @@ verifyConvertBackProof(
uint64_t amount,
uint256 const& contextHash);
/**
* @brief Sequential reader for extracting proof components from a ZKProof blob.
*
* Encapsulates the offset-based arithmetic for slicing a concatenated proof
* blob into its individual components (equality proofs, Pedersen linkage
* proofs, bulletproofs, etc.). Performs bounds checking on every read and
* tracks whether the entire blob has been consumed.
*
* Usage:
* @code
* ProofReader reader(tx[sfZKProof]);
* auto equalityProof = reader.read(sizeEquality);
* auto pedersenProof = reader.read(ecPedersenProofLength);
* if (!equalityProof || !pedersenProof || !reader.done())
* return tecINTERNAL;
* @endcode
*/
class ProofReader
{
Slice data_;
std::size_t offset_ = 0;
public:
explicit ProofReader(Slice data) : data_(data)
{
}
/**
* @brief Read the next @p length bytes from the proof blob.
*
* @param length Number of bytes to read.
* @return A Slice of the requested bytes, or std::nullopt if there are
* not enough remaining bytes.
*/
[[nodiscard]] std::optional<Slice>
read(std::size_t length)
{
if (offset_ + length > data_.size())
return std::nullopt;
auto result = data_.substr(offset_, length);
offset_ += length;
return result;
}
/**
* @brief Returns true when every byte has been consumed.
*/
[[nodiscard]] bool
done() const
{
return offset_ == data_.size();
}
};
} // namespace xrpl

View File

@@ -313,9 +313,6 @@ std::size_t constexpr ecGamalEncryptedLength = 33;
/** EC ElGamal ciphertext length: two 33-byte components concatenated */
std::size_t constexpr ecGamalEncryptedTotalLength = ecGamalEncryptedLength * 2;
/** Length of equality ZKProof in bytes */
std::size_t constexpr ecEqualityProofLength = 98;
/** Length of EC point (compressed) */
std::size_t constexpr compressedECPointLength = 33;
@@ -328,11 +325,8 @@ std::size_t constexpr ecPrivKeyLength = 32;
/** Length of the EC blinding factor in bytes */
std::size_t constexpr ecBlindingFactorLength = 32;
/** Length of Schnorr ZKProof for public key registration in bytes */
std::size_t constexpr ecSchnorrProofLength = 65;
/** Length of ElGamal Pedersen linkage proof in bytes */
std::size_t constexpr ecPedersenProofLength = 195;
/** Length of Schnorr ZKProof for public key registration (compact form) in bytes */
std::size_t constexpr ecSchnorrProofLength = 64;
/** Length of Pedersen Commitment (compressed) */
std::size_t constexpr ecPedersenCommitmentLength = compressedECPointLength;
@@ -343,6 +337,17 @@ std::size_t constexpr ecSingleBulletproofLength = 688;
/** Length of double bulletproof (range proof for 2 commitments) in bytes */
std::size_t constexpr ecDoubleBulletproofLength = 754;
/** Length of the ZKProof for ConfidentialMPTSend.
* 192 bytes compact sigma proof + 754 bytes double bulletproof. */
std::size_t constexpr ecSendProofLength = 946;
/** Length of the ZKProof for ConfidentialMPTConvertBack.
* 128 bytes compact sigma proof + 688 bytes single bulletproof. */
std::size_t constexpr ecConvertBackProofLength = 816;
/** Length of the ZKProof for ConfidentialMPTClawback. */
std::size_t constexpr ecClawbackProofLength = 64;
/** Compressed EC point prefix for even y-coordinate */
std::uint8_t constexpr ecCompressedPrefixEvenY = 0x02;

View File

@@ -32,9 +32,6 @@ namespace xrpl {
*/
class ConfidentialMPTSend : public Transactor
{
/// Size of two Pedersen linkage proofs (amount + balance)
static constexpr std::size_t doublePedersenProofLength = 2 * ecPedersenProofLength;
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};

View File

@@ -4,6 +4,8 @@
#include <openssl/rand.h>
#include <utility/mpt_utility.h>
#include <secp256k1_mpt.h>
namespace xrpl {
/**
@@ -277,7 +279,6 @@ verifyRevealedAmount(
auto const holderP = toParticipant(holder);
auto const issuerP = toParticipant(issuer);
mpt_confidential_participant auditorP;
mpt_confidential_participant const* auditorPtr = nullptr;
if (auditor)
@@ -337,7 +338,7 @@ verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 co
}
TER
verifyClawbackEqualityProof(
verifyClawbackProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
@@ -345,7 +346,7 @@ verifyClawbackEqualityProof(
uint256 const& contextHash)
{
if (ciphertext.size() != ecGamalEncryptedTotalLength || pubKeySlice.size() != ecPubKeyLength ||
proof.size() != ecEqualityProofLength)
proof.size() != ecClawbackProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (mpt_verify_clawback_proof(
@@ -368,10 +369,7 @@ verifySendProof(
uint256 const& contextHash)
{
auto const recipientCount = getConfidentialRecipientCount(auditor.has_value());
auto const expectedProofSize = getEqualityProofSize(recipientCount) +
2 * ecPedersenProofLength + ecDoubleBulletproofLength;
if (proof.size() != expectedProofSize || sender.publicKey.size() != ecPubKeyLength ||
if (proof.size() != ecSendProofLength || sender.publicKey.size() != ecPubKeyLength ||
sender.encryptedAmount.size() != ecGamalEncryptedTotalLength ||
destination.publicKey.size() != ecPubKeyLength ||
destination.encryptedAmount.size() != ecGamalEncryptedTotalLength ||
@@ -403,7 +401,6 @@ verifySendProof(
if (mpt_verify_send_proof(
proof.data(),
proof.size(),
participants.data(),
static_cast<uint8_t>(recipientCount),
spendingBalance.data(),
@@ -424,8 +421,7 @@ verifyConvertBackProof(
uint64_t amount,
uint256 const& contextHash)
{
if (proof.size() != ecPedersenProofLength + ecSingleBulletproofLength ||
pubKeySlice.size() != ecPubKeyLength ||
if (proof.size() != ecConvertBackProofLength || pubKeySlice.size() != ecPubKeyLength ||
spendingBalance.size() != ecGamalEncryptedTotalLength ||
balanceCommitment.size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE

View File

@@ -31,7 +31,7 @@ ConfidentialMPTClawback::preflight(PreflightContext const& ctx)
return temBAD_AMOUNT;
// Verify proof length
if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
if (ctx.tx[sfZKProof].length() != ecClawbackProofLength)
return temMALFORMED;
return tesSUCCESS;
@@ -95,7 +95,7 @@ ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx)
// Verify the revealed confidential amount by the issuer matches the exact
// confidential balance of the holder.
return verifyClawbackEqualityProof(
return verifyClawbackProof(
amount,
ctx.tx[sfZKProof],
(*sleIssuance)[sfIssuerEncryptionKey],

View File

@@ -33,8 +33,8 @@ ConfidentialMPTConvertBack::preflight(PreflightContext const& ctx)
if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
return res;
// ConvertBack proof = pedersen linkage proof + single bulletproof
if (ctx.tx[sfZKProof].size() != ecPedersenProofLength + ecSingleBulletproofLength)
// ConvertBack proof = compact sigma proof (128 bytes) + single bulletproof (688 bytes)
if (ctx.tx[sfZKProof].size() != ecConvertBackProofLength)
return temMALFORMED;
return tesSUCCESS;

View File

@@ -39,12 +39,8 @@ ConfidentialMPTSend::preflight(PreflightContext const& ctx)
if (hasAuditor && ctx.tx[sfAuditorEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
// Check the length of the ZKProof
auto const recipientCount = getConfidentialRecipientCount(hasAuditor);
auto const sizeEquality = getEqualityProofSize(recipientCount);
if (ctx.tx[sfZKProof].length() !=
sizeEquality + doublePedersenProofLength + ecDoubleBulletproofLength)
// Check the length of the ZKProof (fixed size regardless of recipient count)
if (ctx.tx[sfZKProof].length() != ecSendProofLength)
return temMALFORMED;
// Check the Pedersen commitments are valid

View File

@@ -71,19 +71,15 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
}
std::string
getTrivialSendProofHex(size_t nRecipients)
getTrivialSendProofHex()
{
size_t const sizeEquality = getEqualityProofSize(nRecipients);
size_t const totalSize =
sizeEquality + (2 * ecPedersenProofLength) + ecDoubleBulletproofLength;
Buffer buf(ecSendProofLength);
std::memset(buf.data(), 0, ecSendProofLength);
Buffer buf(totalSize);
std::memset(buf.data(), 0, totalSize);
for (std::size_t i = 0; i < totalSize; i += ecGamalEncryptedLength)
for (std::size_t i = 0; i < ecSendProofLength; i += ecGamalEncryptedLength)
{
buf.data()[i] = ecCompressedPrefixEvenY;
if (i + ecGamalEncryptedLength - 1 < totalSize)
if (i + ecGamalEncryptedLength - 1 < ecSendProofLength)
buf.data()[i + ecGamalEncryptedLength - 1] = 0x01;
}
@@ -2113,7 +2109,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = makeZeroBuffer(ecGamalEncryptedTotalLength),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -2125,7 +2121,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.destEncryptedAmt = makeZeroBuffer(ecGamalEncryptedTotalLength),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -2137,7 +2133,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.issuerEncryptedAmt = makeZeroBuffer(ecGamalEncryptedTotalLength),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -2160,7 +2156,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.amountCommitment = makeZeroBuffer(100),
.balanceCommitment = getTrivialCommitment(),
.err = temMALFORMED,
@@ -2171,7 +2167,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = makeZeroBuffer(100),
.err = temMALFORMED,
@@ -2182,7 +2178,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.amountCommitment = makeZeroBuffer(ecPedersenCommitmentLength),
.balanceCommitment = getTrivialCommitment(),
.err = temMALFORMED,
@@ -2193,7 +2189,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = makeZeroBuffer(ecPedersenCommitmentLength),
.err = temMALFORMED,
@@ -2255,7 +2251,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(4),
.proof = getTrivialSendProofHex(),
.auditorEncryptedAmt = makeZeroBuffer(10),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -2267,7 +2263,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(4),
.proof = getTrivialSendProofHex(),
.auditorEncryptedAmt = getBadCiphertext(),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -2382,7 +2378,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
jv[sfIssuerEncryptedAmount] = strHex(getTrivialCiphertext());
jv[sfAmountCommitment] = strHex(getTrivialCommitment());
jv[sfBalanceCommitment] = strHex(getTrivialCommitment());
jv[sfZKProof] = getTrivialSendProofHex(3);
jv[sfZKProof] = getTrivialSendProofHex();
env(jv, ter(tecOBJECT_NOT_FOUND));
}
@@ -2394,7 +2390,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = unknown,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = getTrivialCiphertext(),
.destEncryptedAmt = getTrivialCiphertext(),
.issuerEncryptedAmt = getTrivialCiphertext(),
@@ -2410,7 +2406,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = dave,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = getTrivialCiphertext(),
.destEncryptedAmt = getTrivialCiphertext(),
.issuerEncryptedAmt = getTrivialCiphertext(),
@@ -2422,7 +2418,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = dave,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = getTrivialCiphertext(),
.destEncryptedAmt = getTrivialCiphertext(),
.issuerEncryptedAmt = getTrivialCiphertext(),
@@ -2438,7 +2434,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = eve,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = getTrivialCiphertext(),
.destEncryptedAmt = getTrivialCiphertext(),
.issuerEncryptedAmt = getTrivialCiphertext(),
@@ -2702,7 +2698,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.err = tecBAD_PROOF,
});
}
@@ -2713,7 +2709,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(4),
.proof = getTrivialSendProofHex(),
.auditorEncryptedAmt = getTrivialCiphertext(),
.err = tecNO_PERMISSION,
});
@@ -2773,7 +2769,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(4),
.proof = getTrivialSendProofHex(),
.auditorEncryptedAmt = getTrivialCiphertext(),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -2987,7 +2983,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 0,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = mptAlice.encryptAmount(bob, 0, bf),
.destEncryptedAmt = mptAlice.encryptAmount(carol, 0, bf),
.issuerEncryptedAmt = mptAlice.encryptAmount(alice, 0, bf),
@@ -3005,7 +3001,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 0,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = mptAlice.encryptAmount(bob, 0, bf2),
.destEncryptedAmt = mptAlice.encryptAmount(carol, 0, bf2),
.issuerEncryptedAmt = mptAlice.encryptAmount(alice, 0, bf2),
@@ -4976,7 +4972,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
jv[sfHolder] = bob.human();
jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
jv[sfMPTAmount] = std::to_string(10);
std::string const dummyProof(196, '0');
std::string const dummyProof(ecClawbackProofLength * 2, '0');
jv[sfZKProof] = dummyProof;
jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
@@ -5562,25 +5558,10 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
auto const version = mptAlice.getMPTokenVersion(bob);
// These tests verify that the pedersen linkage proof validation
// These tests verify that the compact ConvertBack proof validation
// correctly rejects proofs generated with incorrect parameters.
// The pedersen linkage proof proves that the balance commitment
// PC = balance*G + rho*H is derived from the holder's encrypted
// spending balance.
// Helper to combine pedersen proof and bulletproof
auto const combineProofs = [](Buffer const& pedersenProof, Buffer const& bulletproof) {
Buffer combinedProof(pedersenProof.size() + bulletproof.size());
std::memcpy(combinedProof.data(), pedersenProof.data(), pedersenProof.size());
std::memcpy(
combinedProof.data() + pedersenProof.size(),
bulletproof.data(),
bulletproof.size());
return combinedProof;
};
auto const holderPubKey = mptAlice.getPubKey(bob);
BEAST_EXPECT(holderPubKey.has_value());
// The compact proof simultaneously verifies balance ownership,
// commitment linkage, and that remaining balance is non-negative.
// Test 1: Proof generated with wrong pedersen commitment value.
// The proof uses PC(1, rho) but the transaction submits PC(balance, rho).
@@ -5710,13 +5691,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
// sequence, issuanceID, amount, version). Using a different context hash
// makes the proof invalid for this transaction, preventing replay attacks.
{
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
uint256 const badContextHash{1};
Buffer const pedersenProof = mptAlice.getBalanceLinkageProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
badContextHash, // wrong context hash
*holderPubKey,
{
.pedersenCommitment = pedersenCommitment,
.amt = *spendingBalance,
@@ -5724,12 +5704,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.blindingFactor = pcBlindingFactor,
});
// Bulletproof uses correct context hash so only pedersen proof fails
Buffer const bulletproof =
mptAlice.getBulletproof({*spendingBalance - amt}, {pcBlindingFactor}, contextHash);
Buffer const proof = combineProofs(pedersenProof, bulletproof);
mptAlice.convertBack({
.account = bob,
.amt = amt,
@@ -5828,110 +5802,88 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
auto const version = mptAlice.getMPTokenVersion(bob);
// These tests verify that the bulletproof (range proof) validation
// These tests verify that the compact ConvertBack proof (sigma + bulletproof)
// correctly rejects proofs generated with incorrect parameters.
// The bulletproof proves that the remaining balance (balance - amount)
// is non-negative, i.e., in the range [0, 2^64-1]. This prevents
// overdrafts where a user tries to convert back more than they have.
// The compact proof simultaneously verifies balance ownership, commitment
// linkage, and that the remaining balance is non-negative.
// Helper to combine pedersen proof and bulletproof
auto const combineProofs = [](Buffer const& pedersenProof, Buffer const& bulletproof) {
Buffer combinedProof(pedersenProof.size() + bulletproof.size());
std::memcpy(combinedProof.data(), pedersenProof.data(), pedersenProof.size());
std::memcpy(
combinedProof.data() + pedersenProof.size(),
bulletproof.data(),
bulletproof.size());
return combinedProof;
};
// Test 1: Proof generated with wrong balance value.
// The sigma proof claims balance=1 but the spending balance contains the
// actual balance. The compact proof's balance-linkage check fails.
{
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
auto const holderPubKey = mptAlice.getPubKey(bob);
BEAST_EXPECT(holderPubKey.has_value());
// Helper to generate pedersen proof with correct parameters.
// The pedersen proof links the encrypted balance to the pedersen commitment.
auto const getPedersenProof = [&](uint256 const& contextHash) {
return mptAlice.getBalanceLinkageProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
*holderPubKey,
{
.pedersenCommitment = pedersenCommitment,
.amt = 1, // wrong balance (actual balance is ~40)
.encryptedAmt = *encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
mptAlice.convertBack({
.account = bob,
.amt = amt,
.proof = proof,
.holderEncryptedAmt = bobCiphertext,
.issuerEncryptedAmt = issuerCiphertext,
.blindingFactor = blindingFactor,
.pedersenCommitment = pedersenCommitment,
.err = tecBAD_PROOF,
});
}
// Test 2: Proof generated with wrong blinding factor (rho).
// The compact sigma proof must use the same blinding factor (rho) as the
// Pedersen commitment PC = balance*G + rho*H. Using a different rho
// creates an inconsistency the verifier detects.
{
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
{
.pedersenCommitment = pedersenCommitment,
.amt = *spendingBalance,
.encryptedAmt = *encryptedSpendingBalance,
.blindingFactor = generateBlindingFactor(), // wrong blinding factor
});
mptAlice.convertBack({
.account = bob,
.amt = amt,
.proof = proof,
.holderEncryptedAmt = bobCiphertext,
.issuerEncryptedAmt = issuerCiphertext,
.blindingFactor = blindingFactor,
.pedersenCommitment = pedersenCommitment,
.err = tecBAD_PROOF,
});
}
// Test 3: Proof generated with wrong context hash.
// The context hash binds the proof to a specific transaction (account,
// sequence, issuanceID, amount, version). Using a different context hash
// makes the proof invalid for this transaction, preventing replay attacks.
{
uint256 const badContextHash{1};
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
badContextHash, // wrong context hash
{
.pedersenCommitment = pedersenCommitment,
.amt = *spendingBalance,
.encryptedAmt = *encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
};
// Test 1: Bulletproof generated with wrong remaining balance.
// The bulletproof claims remaining balance is 1, but the pedersen
// commitment was created with (balance - amount). The verifier computes
// PC_rem = PC - amount*G and checks if the bulletproof matches, which fails.
{
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
Buffer const bulletproof = mptAlice.getBulletproof(
{1}, // wrong remaining balance
{pcBlindingFactor},
contextHash);
Buffer const proof = combineProofs(getPedersenProof(contextHash), bulletproof);
mptAlice.convertBack({
.account = bob,
.amt = amt,
.proof = proof,
.holderEncryptedAmt = bobCiphertext,
.issuerEncryptedAmt = issuerCiphertext,
.blindingFactor = blindingFactor,
.pedersenCommitment = pedersenCommitment,
.err = tecBAD_PROOF,
});
}
// Test 2: Bulletproof generated with wrong blinding factor.
// The bulletproof must use the same blinding factor (rho) as the pedersen
// commitment PC = (balance - amount)*G + rho*H. Using a different rho
// creates a commitment mismatch and verification fails.
{
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
Buffer const bulletproof = mptAlice.getBulletproof(
{*spendingBalance - amt},
{generateBlindingFactor()}, // wrong blinding factor
contextHash);
Buffer const proof = combineProofs(getPedersenProof(contextHash), bulletproof);
mptAlice.convertBack({
.account = bob,
.amt = amt,
.proof = proof,
.holderEncryptedAmt = bobCiphertext,
.issuerEncryptedAmt = issuerCiphertext,
.blindingFactor = blindingFactor,
.pedersenCommitment = pedersenCommitment,
.err = tecBAD_PROOF,
});
}
// Test 3: Bulletproof generated with wrong context hash.
// The context hash binds the proof to a specific transaction (account,
// sequence, issuanceID, amount, version). Using a different context hash
// makes the proof invalid for this transaction, preventing replay attacks.
{
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
uint256 const badContextHash{1};
Buffer const bulletproof = mptAlice.getBulletproof(
{*spendingBalance - amt},
{pcBlindingFactor},
badContextHash); // wrong context hash
Buffer const proof = combineProofs(getPedersenProof(contextHash), bulletproof);
mptAlice.convertBack({
.account = bob,
@@ -6627,7 +6579,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = getBadCiphertext(),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6639,7 +6591,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.destEncryptedAmt = getBadCiphertext(),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6651,7 +6603,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.issuerEncryptedAmt = getBadCiphertext(),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6670,7 +6622,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.amountCommitment = badCommitment,
.balanceCommitment = getTrivialCommitment(),
.err = temMALFORMED,
@@ -6680,7 +6632,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = badCommitment,
.err = temMALFORMED,
@@ -6714,10 +6666,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
{.account = carol, .amt = 30, .holderPubKey = mptAlice.getPubKey(carol)});
mptAlice.mergeInbox({.account = carol});
size_t const proofSize =
getEqualityProofSize(3) + 2 * ecPedersenProofLength + ecDoubleBulletproofLength;
Buffer badProof(proofSize);
std::memset(badProof.data(), 0xFF, proofSize);
Buffer badProof(ecSendProofLength);
std::memset(badProof.data(), 0xFF, ecSendProofLength);
badProof.data()[0] = ecCompressedPrefixEvenY;
mptAlice.send({
@@ -6777,7 +6727,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = badC1goodC2,
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6789,7 +6739,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = goodC1badC2,
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6801,7 +6751,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.destEncryptedAmt = badC1goodC2,
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6813,7 +6763,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.destEncryptedAmt = goodC1badC2,
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6891,7 +6841,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.senderEncryptedAmt = wrongGroupCt,
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6903,7 +6853,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.destEncryptedAmt = wrongGroupCt,
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6915,7 +6865,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.issuerEncryptedAmt = wrongGroupCt,
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = getTrivialCommitment(),
@@ -6927,7 +6877,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.amountCommitment = wrongGroupCommitment,
.balanceCommitment = getTrivialCommitment(),
.err = tecBAD_PROOF,
@@ -6938,7 +6888,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.proof = getTrivialSendProofHex(),
.amountCommitment = getTrivialCommitment(),
.balanceCommitment = wrongGroupCommitment,
.err = tecBAD_PROOF,
@@ -8405,7 +8355,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
jv[sfHolder] = bob.human();
jv[sfMPTAmount.jsonName] = "50";
jv[sfZKProof.jsonName] = std::string(ecEqualityProofLength * 2, '0');
jv[sfZKProof.jsonName] = std::string(ecClawbackProofLength * 2, '0');
env(jv, delegate::as(dave), ter(temMALFORMED));
}
@@ -8418,7 +8368,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
jv[sfHolder] = carol.human();
jv[sfMPTAmount.jsonName] = "100";
jv[sfZKProof.jsonName] = std::string(ecEqualityProofLength * 2, '0');
jv[sfZKProof.jsonName] = std::string(ecClawbackProofLength * 2, '0');
env(jv, delegate::as(dave), ter(temMALFORMED));
}
}

View File

@@ -792,7 +792,7 @@ MPTTester::getClawbackProof(
if (pubKeyBlob.size() != ecPubKeyLength)
return std::nullopt;
Buffer proof(ecEqualityProofLength);
Buffer proof(ecClawbackProofLength);
if (mpt_get_clawback_proof(
privateKey.data(),
@@ -838,7 +838,6 @@ MPTTester::getConfidentialSendProof(
PedersenProofParams const& amountParams,
PedersenProofParams const& balanceParams) const
{
auto const pedersenAmountParams = makePedersenParams(amountParams);
auto const pedersenBalanceParams = makePedersenParams(balanceParams);
if (recipients.size() != nRecipients)
return std::nullopt;
@@ -850,6 +849,13 @@ MPTTester::getConfidentialSendProof(
if (!senderPrivKey)
return std::nullopt;
auto const senderPubKey = getPubKey(sender);
if (!senderPubKey || senderPubKey->size() != ecPubKeyLength)
return std::nullopt;
if (amountParams.pedersenCommitment.size() != ecPedersenCommitmentLength)
return std::nullopt;
// Build mpt_confidential_participant array
std::vector<mpt_confidential_participant> participants(nRecipients);
for (size_t i = 0; i < nRecipients; ++i)
@@ -862,17 +868,18 @@ MPTTester::getConfidentialSendProof(
std::memcpy(participants[i].ciphertext, r.encryptedAmount.data(), kMPT_ELGAMAL_TOTAL_SIZE);
}
size_t proofLen = get_confidential_send_proof_size(nRecipients);
size_t proofLen = ecSendProofLength;
Buffer proof(proofLen);
if (mpt_get_confidential_send_proof(
senderPrivKey->data(),
senderPubKey->data(),
amount,
participants.data(),
nRecipients,
blindingFactor.data(),
contextHash.data(),
&pedersenAmountParams,
amountParams.pedersenCommitment.data(),
&pedersenBalanceParams,
proof.data(),
&proofLen) != 0)
@@ -914,8 +921,8 @@ MPTTester::getConvertBackProof(
uint256 const& contextHash,
PedersenProofParams const& pcParams) const
{
// Expected total proof length: pedersen proof + single bulletproof
std::size_t constexpr expectedProofLength = ecPedersenProofLength + ecSingleBulletproofLength;
// Expected total proof length: compact sigma proof (128 bytes) + single bulletproof (688 bytes)
std::size_t constexpr expectedProofLength = ecConvertBackProofLength;
auto const sleMptoken = env_.le(keylet::mptoken(*id_, holder.id()));
if (!sleMptoken || !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
@@ -1317,12 +1324,14 @@ MPTTester::send(MPTConfidentialSend const& arg)
}
// Fill in the commitment if not provided
// The amount commitment must use the same blinding factor as the ElGamal
// encryption. The sigma proof links the two, so using different randomness
// for each would cause proof verification to fail.
Buffer amountCommitment, balanceCommitment;
auto const amountBlindingFactor = generateBlindingFactor();
if (arg.amountCommitment)
amountCommitment = *arg.amountCommitment;
else
amountCommitment = getPedersenCommitment(*arg.amt, amountBlindingFactor);
amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor);
jv[sfAmountCommitment] = strHex(amountCommitment);
@@ -1390,7 +1399,7 @@ MPTTester::send(MPTConfidentialSend const& arg)
blindingFactor,
nRecipients,
ctxHash,
{amountCommitment, *arg.amt, senderAmt, amountBlindingFactor},
{amountCommitment, *arg.amt, senderAmt, blindingFactor},
{balanceCommitment,
*prevSenderSpending,
*prevEncryptedSenderSpending,
@@ -1401,11 +1410,7 @@ MPTTester::send(MPTConfidentialSend const& arg)
jv[sfZKProof.jsonName] = strHex(*proof);
else
{
size_t const sizeEquality = secp256k1_mpt_proof_equality_shared_r_size(nRecipients);
size_t const dummySize =
sizeEquality + 2 * ecPedersenProofLength + ecDoubleBulletproofLength;
jv[sfZKProof.jsonName] = strHex(makeZeroBuffer(dummySize));
jv[sfZKProof.jsonName] = strHex(makeZeroBuffer(ecSendProofLength));
}
}
@@ -1581,12 +1586,13 @@ MPTTester::sendJV(
version = getMPTokenVersion(*arg.account);
}
// The amount commitment must use the same blinding factor as the tx ElGamal
// encryption blinding factor.
Buffer amountCommitment, balanceCommitment;
auto const amountBlindingFactor = generateBlindingFactor();
if (arg.amountCommitment)
amountCommitment = *arg.amountCommitment;
else
amountCommitment = getPedersenCommitment(*arg.amt, amountBlindingFactor);
amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor);
jv[sfAmountCommitment] = strHex(amountCommitment);
@@ -1641,7 +1647,7 @@ MPTTester::sendJV(
blindingFactor,
nRecipients,
ctxHash,
{amountCommitment, *arg.amt, senderAmt, amountBlindingFactor},
{amountCommitment, *arg.amt, senderAmt, blindingFactor},
{balanceCommitment,
prevSenderSpending,
*prevEncryptedSenderSpending,
@@ -1651,12 +1657,7 @@ MPTTester::sendJV(
if (proof)
jv[sfZKProof.jsonName] = strHex(*proof);
else
{
size_t const sizeEquality = secp256k1_mpt_proof_equality_shared_r_size(nRecipients);
size_t const dummySize =
sizeEquality + 2 * ecPedersenProofLength + ecDoubleBulletproofLength;
jv[sfZKProof.jsonName] = strHex(makeZeroBuffer(dummySize));
}
jv[sfZKProof.jsonName] = strHex(makeZeroBuffer(ecSendProofLength));
}
return jv;
@@ -1751,7 +1752,7 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg)
if (proof)
jv[sfZKProof] = strHex(*proof);
else
jv[sfZKProof] = strHex(makeZeroBuffer(ecEqualityProofLength));
jv[sfZKProof] = strHex(makeZeroBuffer(ecClawbackProofLength));
}
auto const holderPubAmt = getBalance(*arg.holder);
@@ -2066,7 +2067,7 @@ MPTTester::convertBack(MPTConvertBack const& arg)
// generate a dummy proof if no encrypted amount field, so that other
// preflight/preclaim are checked
if (!prevEncryptedSpendingBalance)
proof = makeZeroBuffer(ecPedersenProofLength + ecSingleBulletproofLength);
proof = makeZeroBuffer(ecConvertBackProofLength);
else
{
proof = getConvertBackProof(
@@ -2198,7 +2199,7 @@ MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq)
Buffer proof;
if (!prevEncSpending)
proof = makeZeroBuffer(ecPedersenProofLength + ecSingleBulletproofLength);
proof = makeZeroBuffer(ecConvertBackProofLength);
else
proof = getConvertBackProof(
*arg.account,
@@ -2217,110 +2218,6 @@ MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq)
return jv;
}
Buffer
MPTTester::getAmountLinkageProof(
Buffer const& pubKey,
Buffer const& blindingFactor,
uint256 const& contextHash,
PedersenProofParams const& params) const
{
if (pubKey.size() != ecPubKeyLength || blindingFactor.size() != ecBlindingFactorLength)
return makeZeroBuffer(ecPedersenProofLength);
auto const pedersenParams = makePedersenParams(params);
Buffer proof(ecPedersenProofLength);
if (mpt_get_amount_linkage_proof(
pubKey.data(),
blindingFactor.data(),
contextHash.data(),
&pedersenParams,
proof.data()) != 0)
{
Throw<std::runtime_error>("Amount Linkage Proof generation failed");
}
return proof;
}
Buffer
MPTTester::getBalanceLinkageProof(
Account const& account,
uint256 const& contextHash,
Buffer const& pubKey,
PedersenProofParams const& params) const
{
if (pubKey.size() != ecPubKeyLength)
return makeZeroBuffer(ecPedersenProofLength);
auto const privKey = getPrivKey(account);
if (!privKey || privKey->size() != ecPrivKeyLength)
Throw<std::runtime_error>("Failed to get Pedersen proof private key");
auto const pedersenParams = makePedersenParams(params);
Buffer proof(ecPedersenProofLength);
if (mpt_get_balance_linkage_proof(
privKey->data(), pubKey.data(), contextHash.data(), &pedersenParams, proof.data()) != 0)
Throw<std::runtime_error>("Pedersen proof generation failed");
return proof;
}
Buffer
MPTTester::getBulletproof(
std::vector<std::uint64_t> const& values,
std::vector<Buffer> const& blindingFactors,
uint256 const& contextHash) const
{
std::size_t const m = values.size();
if (m == 0 || m > 2 || m != blindingFactors.size())
Throw<std::runtime_error>("getBulletproof: invalid input parameters");
for (auto const& bf : blindingFactors)
{
if (bf.size() != ecBlindingFactorLength)
Throw<std::runtime_error>("Invalid blinding factor length");
}
// Flatten blinding factors into contiguous memory (m * 32 bytes)
std::vector<unsigned char> blindingsFlat(m * ecBlindingFactorLength);
for (std::size_t i = 0; i < m; ++i)
std::memcpy(
blindingsFlat.data() + i * ecBlindingFactorLength,
blindingFactors[i].data(),
ecBlindingFactorLength);
secp256k1_pubkey pk_base;
if (secp256k1_mpt_get_h_generator(secp256k1Context(), &pk_base) != 1)
Throw<std::runtime_error>("Failed to get H generator");
// Proof size scales with m; use safe upper bound
Buffer bulletproof(4096);
std::size_t proofLen = 4096;
if (secp256k1_bulletproof_prove_agg(
secp256k1Context(),
bulletproof.data(),
&proofLen,
values.data(),
blindingsFlat.data(),
m,
&pk_base,
contextHash.data()) != 1)
{
Throw<std::runtime_error>("Bulletproof generation failed");
}
std::size_t const expectedLen =
(m == 1) ? ecSingleBulletproofLength : ecDoubleBulletproofLength;
if (proofLen != expectedLen)
Throw<std::runtime_error>("Unexpected bulletproof length");
return Buffer(bulletproof.data(), proofLen);
}
} // namespace jtx
} // namespace test
} // namespace xrpl

View File

@@ -582,26 +582,6 @@ public:
std::uint32_t
getMPTokenVersion(Account const account) const;
Buffer
getAmountLinkageProof(
Buffer const& pubKey,
Buffer const& blindingFactor,
uint256 const& contextHash,
PedersenProofParams const& params) const;
Buffer
getBalanceLinkageProof(
Account const& account,
uint256 const& contextHash,
Buffer const& pubKey,
PedersenProofParams const& params) const;
Buffer
getBulletproof(
std::vector<std::uint64_t> const& values,
std::vector<Buffer> const& blindingFactors,
uint256 const& contextHash) const;
Buffer
getPedersenCommitment(std::uint64_t const amount, Buffer const& pedersenBlindingFactor);