mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Support Range Proof for ConfidentialMPTSend (#6404)
- proving send amount m is in the range [0, 2^64) - proving remaining balance b-m is in the range [0, 2^64)
This commit is contained in:
@@ -54,6 +54,7 @@ words:
|
||||
- autobridging
|
||||
- bimap
|
||||
- bindir
|
||||
- blindings
|
||||
- bookdir
|
||||
- Bougalis
|
||||
- Britto
|
||||
@@ -249,6 +250,7 @@ words:
|
||||
- stvar
|
||||
- stvector
|
||||
- stxchainattestations
|
||||
- summands
|
||||
- superpeer
|
||||
- superpeers
|
||||
- takergets
|
||||
|
||||
@@ -469,6 +469,24 @@ verifyAggregatedBulletproof(
|
||||
std::vector<Slice> const& compressedCommitments,
|
||||
uint256 const& contextHash);
|
||||
|
||||
/**
|
||||
* @brief Computes the remainder commitment for ConfidentialMPTSend.
|
||||
*
|
||||
* 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 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).
|
||||
* @param out Output buffer for the resulting remainder commitment (33 bytes).
|
||||
* @return tesSUCCESS on success, tecINTERNAL on failure.
|
||||
*/
|
||||
TER
|
||||
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out);
|
||||
|
||||
/**
|
||||
* @brief Computes the remainder commitment for ConvertBack.
|
||||
*
|
||||
|
||||
@@ -579,6 +579,41 @@ verifyAggregatedBulletproof(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out)
|
||||
{
|
||||
if (balanceCommitment.size() != ecPedersenCommitmentLength || amountCommitment.size() != ecPedersenCommitmentLength)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const ctx = secp256k1Context();
|
||||
|
||||
secp256k1_pubkey pcBalance;
|
||||
if (secp256k1_ec_pubkey_parse(ctx, &pcBalance, balanceCommitment.data(), ecPedersenCommitmentLength) != 1)
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey pcAmount;
|
||||
if (secp256k1_ec_pubkey_parse(ctx, &pcAmount, amountCommitment.data(), ecPedersenCommitmentLength) != 1)
|
||||
return tecINTERNAL;
|
||||
|
||||
// Negate PC_amount point to get -PC_amount
|
||||
if (!secp256k1_ec_pubkey_negate(ctx, &pcAmount))
|
||||
return tecINTERNAL;
|
||||
|
||||
// Compute pcRem = pcBalance + (-pcAmount)
|
||||
secp256k1_pubkey const* summands[2] = {&pcBalance, &pcAmount};
|
||||
secp256k1_pubkey pcRem;
|
||||
if (!secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2))
|
||||
return tecINTERNAL;
|
||||
|
||||
// Serialize result to compressed format
|
||||
out.alloc(ecPedersenCommitmentLength);
|
||||
size_t outLen = ecPedersenCommitmentLength;
|
||||
if (secp256k1_ec_pubkey_serialize(ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED) != 1)
|
||||
return tecINTERNAL;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
computeConvertBackRemainder(Slice const& commitment, std::uint64_t amount, Buffer& out)
|
||||
{
|
||||
|
||||
@@ -72,7 +72,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
getTrivialSendProofHex(size_t nRecipients)
|
||||
{
|
||||
size_t const sizeEquality = getMultiCiphertextEqualityProofSize(nRecipients);
|
||||
size_t const totalSize = sizeEquality + (2 * ecPedersenProofLength);
|
||||
size_t const totalSize = sizeEquality + (2 * ecPedersenProofLength) + ecDoubleBulletproofLength;
|
||||
|
||||
Buffer buf(totalSize);
|
||||
std::memset(buf.data(), 0, totalSize);
|
||||
@@ -1721,6 +1721,71 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSendRangeProof(FeatureBitset features)
|
||||
{
|
||||
testcase("test ConfidentialMPTSend Range Proof");
|
||||
|
||||
using namespace test::jtx;
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
||||
|
||||
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanPrivacy | tfMPTCanTransfer});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = carol});
|
||||
|
||||
mptAlice.pay(alice, bob, 1000);
|
||||
mptAlice.pay(alice, carol, 1000);
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(carol);
|
||||
|
||||
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
|
||||
|
||||
{
|
||||
// Bob converts 60
|
||||
mptAlice.convert({.account = bob, .amt = 60, .holderPubKey = mptAlice.getPubKey(bob)});
|
||||
mptAlice.mergeInbox({.account = bob});
|
||||
|
||||
mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)});
|
||||
mptAlice.mergeInbox({.account = carol});
|
||||
|
||||
// Bob has 60, tries to send 70. Invalid remaining balance.
|
||||
mptAlice.send({.account = bob, .dest = carol, .amt = 70, .err = tecBAD_PROOF});
|
||||
|
||||
// Bob has 60, tries to send 61. Invalid remaining balance.
|
||||
mptAlice.send({.account = bob, .dest = carol, .amt = 61, .err = tecBAD_PROOF});
|
||||
|
||||
// Bob has 60, sends 60. Remainder is exactly 0. Valid remaining balance.
|
||||
mptAlice.send({.account = bob, .dest = carol, .amt = 60, .err = tesSUCCESS});
|
||||
}
|
||||
|
||||
{
|
||||
// Bob converts 100.
|
||||
mptAlice.convert({.account = bob, .amt = 100});
|
||||
mptAlice.mergeInbox({.account = bob});
|
||||
|
||||
// Bob has 100, tries to send 2^64-1. Invalid remaining balance.
|
||||
mptAlice.send(
|
||||
{.account = bob,
|
||||
.dest = carol,
|
||||
.amt = 0xFFFFFFFFFFFFFFFF, // Max uint64
|
||||
.err = tecBAD_PROOF});
|
||||
|
||||
// Bob sends 1, remaining 99.
|
||||
mptAlice.send({.account = bob, .dest = carol, .amt = 1, .err = tesSUCCESS});
|
||||
|
||||
// Bob sends 100, but only has 99. Invalid remaining balance.
|
||||
mptAlice.send({.account = bob, .dest = carol, .amt = 100, .err = tecBAD_PROOF});
|
||||
}
|
||||
|
||||
// todo: test m exceeding range, require using scala and refactor
|
||||
}
|
||||
|
||||
void
|
||||
testDelete(FeatureBitset features)
|
||||
{
|
||||
@@ -3668,11 +3733,13 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
// ConfidentialMPTConvert
|
||||
testConvert(features);
|
||||
testConvertPreflight(features);
|
||||
testConvertPreclaim(features);
|
||||
testConvertWithAuditor(features);
|
||||
|
||||
// ConfidentialMPTMergeInbox
|
||||
testMergeInbox(features);
|
||||
testMergeInboxPreflight(features);
|
||||
testMergeInboxPreclaim(features);
|
||||
@@ -3683,6 +3750,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
testSend(features);
|
||||
testSendPreflight(features);
|
||||
testSendPreclaim(features);
|
||||
testSendRangeProof(features);
|
||||
testSendDepositPreauth(features);
|
||||
testSendWithAuditor(features);
|
||||
|
||||
@@ -3695,6 +3763,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
|
||||
testDelete(features);
|
||||
|
||||
// ConfidentialMPTConvertBack
|
||||
testConvertBack(features);
|
||||
testConvertBackPreflight(features);
|
||||
testConvertBackPreclaim(features);
|
||||
|
||||
@@ -820,10 +820,27 @@ MPTTester::getConfidentialSendProof(
|
||||
|
||||
auto const balanceLinkageProof = getBalanceLinkageProof(sender, contextHash, *senderPubKey, balanceParams);
|
||||
|
||||
std::uint64_t const remainingBalance = balanceParams.amt - amount;
|
||||
|
||||
// Compute the blinding factor for the remaining balance: rho_rem = rho_balance - rho_amount
|
||||
unsigned char rho_rem[32];
|
||||
unsigned char neg_rho_m[32];
|
||||
|
||||
secp256k1_mpt_scalar_negate(neg_rho_m, amountParams.blindingFactor.data());
|
||||
secp256k1_mpt_scalar_add(rho_rem, balanceParams.blindingFactor.data(), neg_rho_m);
|
||||
|
||||
// Generate bulletproof for the amount and remaining balance
|
||||
Buffer const bulletproof =
|
||||
getBulletproof({amount, remainingBalance}, {amountParams.blindingFactor, Buffer(rho_rem, 32)}, contextHash);
|
||||
|
||||
OPENSSL_cleanse(neg_rho_m, 32);
|
||||
OPENSSL_cleanse(rho_rem, 32);
|
||||
|
||||
auto const sizeAmountLinkage = amountLinkageProof.size();
|
||||
auto const sizeBalanceLinkage = balanceLinkageProof.size();
|
||||
auto const sizeBulletproof = bulletproof.size();
|
||||
|
||||
size_t const proofSize = sizeEquality + sizeAmountLinkage + sizeBalanceLinkage;
|
||||
size_t const proofSize = sizeEquality + sizeAmountLinkage + sizeBalanceLinkage + sizeBulletproof;
|
||||
Buffer proof(proofSize);
|
||||
|
||||
auto ptr = proof.data();
|
||||
@@ -834,6 +851,9 @@ MPTTester::getConfidentialSendProof(
|
||||
ptr += sizeAmountLinkage;
|
||||
|
||||
std::memcpy(ptr, balanceLinkageProof.data(), sizeBalanceLinkage);
|
||||
ptr += sizeBalanceLinkage;
|
||||
|
||||
std::memcpy(ptr, bulletproof.data(), sizeBulletproof);
|
||||
|
||||
return proof;
|
||||
}
|
||||
@@ -1298,7 +1318,8 @@ MPTTester::send(MPTConfidentialSend const& arg)
|
||||
jv[sfZKProof.jsonName] = strHex(*proof);
|
||||
else
|
||||
{
|
||||
size_t const dummySize = secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients);
|
||||
size_t const sizeEquality = secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients);
|
||||
size_t const dummySize = sizeEquality + 2 * ecPedersenProofLength + ecDoubleBulletproofLength;
|
||||
|
||||
jv[sfZKProof.jsonName] = strHex(Buffer(dummySize));
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ ConfidentialMPTSend::preflight(PreflightContext const& ctx)
|
||||
auto const sizeEquality = getMultiCiphertextEqualityProofSize(recipientCount);
|
||||
auto const sizePedersenLinkage = 2 * ecPedersenProofLength;
|
||||
|
||||
if (ctx.tx[sfZKProof].length() != sizeEquality + sizePedersenLinkage)
|
||||
if (ctx.tx[sfZKProof].length() != sizeEquality + sizePedersenLinkage + ecDoubleBulletproofLength)
|
||||
return temMALFORMED;
|
||||
|
||||
// Check the Pedersen commitments are valid
|
||||
@@ -105,7 +105,14 @@ verifySendProofs(
|
||||
currentOffset += ecPedersenProofLength;
|
||||
remainingLength -= ecPedersenProofLength;
|
||||
|
||||
// todo: Extract range proof once the lib is ready
|
||||
// Extract range proof
|
||||
if (remainingLength < ecDoubleBulletproofLength)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const rangeProof = proof.substr(currentOffset, ecDoubleBulletproofLength);
|
||||
currentOffset += ecDoubleBulletproofLength;
|
||||
remainingLength -= ecDoubleBulletproofLength;
|
||||
|
||||
if (remainingLength != 0)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -114,9 +121,7 @@ verifySendProofs(
|
||||
recipients.reserve(recipientCount);
|
||||
|
||||
recipients.push_back({(*sleSenderMPToken)[sfHolderElGamalPublicKey], ctx.tx[sfSenderEncryptedAmount]});
|
||||
|
||||
recipients.push_back({(*sleDestinationMPToken)[sfHolderElGamalPublicKey], ctx.tx[sfDestinationEncryptedAmount]});
|
||||
|
||||
recipients.push_back({(*sleIssuance)[sfIssuerElGamalPublicKey], ctx.tx[sfIssuerEncryptedAmount]});
|
||||
|
||||
if (hasAuditor)
|
||||
@@ -132,12 +137,15 @@ 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))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: Equality proof failed.";
|
||||
return ter;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Verify amount linkage
|
||||
@@ -149,8 +157,7 @@ verifySendProofs(
|
||||
contextHash);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: Amount linkage proof failed.";
|
||||
return ter;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Verify balance linkage
|
||||
@@ -162,8 +169,38 @@ verifySendProofs(
|
||||
contextHash);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: Balance linkage proof failed.";
|
||||
return ter;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Verify Range Proof
|
||||
{
|
||||
Buffer pcRem;
|
||||
|
||||
// Derive PC_rem = PC_balance - PC_amount
|
||||
if (auto const ter = computeSendRemainder(ctx.tx[sfBalanceCommitment], ctx.tx[sfAmountCommitment], pcRem);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: One or more cryptographic proofs failed.";
|
||||
return tecBAD_PROOF;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
|
||||
Reference in New Issue
Block a user