Skip to content

Commit 6ad60d7

Browse files
authored
Support Range Proof for ConfidentialMPTSend (XRPLF#6404)
- proving send amount m is in the range [0, 2^64) - proving remaining balance b-m is in the range [0, 2^64)
1 parent 94e911e commit 6ad60d7

File tree

6 files changed

+195
-13
lines changed

6 files changed

+195
-13
lines changed

cspell.config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ words:
5454
- autobridging
5555
- bimap
5656
- bindir
57+
- blindings
5758
- bookdir
5859
- Bougalis
5960
- Britto
@@ -249,6 +250,7 @@ words:
249250
- stvar
250251
- stvector
251252
- stxchainattestations
253+
- summands
252254
- superpeer
253255
- superpeers
254256
- takergets

include/xrpl/protocol/ConfidentialTransfer.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,24 @@ verifyAggregatedBulletproof(
469469
std::vector<Slice> const& compressedCommitments,
470470
uint256 const& contextHash);
471471

472+
/**
473+
* @brief Computes the remainder commitment for ConfidentialMPTSend.
474+
*
475+
* Given a balance commitment PC_bal = m_bal*G + rho_bal*H and an amount
476+
* commitment PC_amt = m_amt*G + rho_amt*H, this function computes:
477+
* PC_rem = PC_bal - PC_amt = (m_bal - m_amt)*G + (rho_bal - rho_amt)*H
478+
*
479+
* This derived commitment is used in an aggregated range proof to ensure
480+
* the sender maintains a non-negative balance (m_bal - m_amt >= 0).
481+
*
482+
* @param balanceCommitment The compressed Pedersen commitment to the balance (33 bytes).
483+
* @param amountCommitment The compressed Pedersen commitment to the amount (33 bytes).
484+
* @param out Output buffer for the resulting remainder commitment (33 bytes).
485+
* @return tesSUCCESS on success, tecINTERNAL on failure.
486+
*/
487+
TER
488+
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out);
489+
472490
/**
473491
* @brief Computes the remainder commitment for ConvertBack.
474492
*

src/libxrpl/protocol/ConfidentialTransfer.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,41 @@ verifyAggregatedBulletproof(
579579
return tesSUCCESS;
580580
}
581581

582+
TER
583+
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out)
584+
{
585+
if (balanceCommitment.size() != ecPedersenCommitmentLength || amountCommitment.size() != ecPedersenCommitmentLength)
586+
return tecINTERNAL;
587+
588+
auto const ctx = secp256k1Context();
589+
590+
secp256k1_pubkey pcBalance;
591+
if (secp256k1_ec_pubkey_parse(ctx, &pcBalance, balanceCommitment.data(), ecPedersenCommitmentLength) != 1)
592+
return tecINTERNAL;
593+
594+
secp256k1_pubkey pcAmount;
595+
if (secp256k1_ec_pubkey_parse(ctx, &pcAmount, amountCommitment.data(), ecPedersenCommitmentLength) != 1)
596+
return tecINTERNAL;
597+
598+
// Negate PC_amount point to get -PC_amount
599+
if (!secp256k1_ec_pubkey_negate(ctx, &pcAmount))
600+
return tecINTERNAL;
601+
602+
// Compute pcRem = pcBalance + (-pcAmount)
603+
secp256k1_pubkey const* summands[2] = {&pcBalance, &pcAmount};
604+
secp256k1_pubkey pcRem;
605+
if (!secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2))
606+
return tecINTERNAL;
607+
608+
// Serialize result to compressed format
609+
out.alloc(ecPedersenCommitmentLength);
610+
size_t outLen = ecPedersenCommitmentLength;
611+
if (secp256k1_ec_pubkey_serialize(ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED) != 1)
612+
return tecINTERNAL;
613+
614+
return tesSUCCESS;
615+
}
616+
582617
TER
583618
computeConvertBackRemainder(Slice const& commitment, std::uint64_t amount, Buffer& out)
584619
{

src/test/app/ConfidentialTransfer_test.cpp

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
7272
getTrivialSendProofHex(size_t nRecipients)
7373
{
7474
size_t const sizeEquality = getMultiCiphertextEqualityProofSize(nRecipients);
75-
size_t const totalSize = sizeEquality + (2 * ecPedersenProofLength);
75+
size_t const totalSize = sizeEquality + (2 * ecPedersenProofLength) + ecDoubleBulletproofLength;
7676

7777
Buffer buf(totalSize);
7878
std::memset(buf.data(), 0, totalSize);
@@ -1721,6 +1721,71 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
17211721
}
17221722
}
17231723

1724+
void
1725+
testSendRangeProof(FeatureBitset features)
1726+
{
1727+
testcase("test ConfidentialMPTSend Range Proof");
1728+
1729+
using namespace test::jtx;
1730+
Env env{*this, features};
1731+
Account const alice("alice");
1732+
Account const bob("bob");
1733+
Account const carol("carol");
1734+
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1735+
1736+
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanPrivacy | tfMPTCanTransfer});
1737+
mptAlice.authorize({.account = bob});
1738+
mptAlice.authorize({.account = carol});
1739+
1740+
mptAlice.pay(alice, bob, 1000);
1741+
mptAlice.pay(alice, carol, 1000);
1742+
1743+
mptAlice.generateKeyPair(alice);
1744+
mptAlice.generateKeyPair(bob);
1745+
mptAlice.generateKeyPair(carol);
1746+
1747+
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1748+
1749+
{
1750+
// Bob converts 60
1751+
mptAlice.convert({.account = bob, .amt = 60, .holderPubKey = mptAlice.getPubKey(bob)});
1752+
mptAlice.mergeInbox({.account = bob});
1753+
1754+
mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)});
1755+
mptAlice.mergeInbox({.account = carol});
1756+
1757+
// Bob has 60, tries to send 70. Invalid remaining balance.
1758+
mptAlice.send({.account = bob, .dest = carol, .amt = 70, .err = tecBAD_PROOF});
1759+
1760+
// Bob has 60, tries to send 61. Invalid remaining balance.
1761+
mptAlice.send({.account = bob, .dest = carol, .amt = 61, .err = tecBAD_PROOF});
1762+
1763+
// Bob has 60, sends 60. Remainder is exactly 0. Valid remaining balance.
1764+
mptAlice.send({.account = bob, .dest = carol, .amt = 60, .err = tesSUCCESS});
1765+
}
1766+
1767+
{
1768+
// Bob converts 100.
1769+
mptAlice.convert({.account = bob, .amt = 100});
1770+
mptAlice.mergeInbox({.account = bob});
1771+
1772+
// Bob has 100, tries to send 2^64-1. Invalid remaining balance.
1773+
mptAlice.send(
1774+
{.account = bob,
1775+
.dest = carol,
1776+
.amt = 0xFFFFFFFFFFFFFFFF, // Max uint64
1777+
.err = tecBAD_PROOF});
1778+
1779+
// Bob sends 1, remaining 99.
1780+
mptAlice.send({.account = bob, .dest = carol, .amt = 1, .err = tesSUCCESS});
1781+
1782+
// Bob sends 100, but only has 99. Invalid remaining balance.
1783+
mptAlice.send({.account = bob, .dest = carol, .amt = 100, .err = tecBAD_PROOF});
1784+
}
1785+
1786+
// todo: test m exceeding range, require using scala and refactor
1787+
}
1788+
17241789
void
17251790
testDelete(FeatureBitset features)
17261791
{
@@ -3668,11 +3733,13 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
36683733
void
36693734
testWithFeats(FeatureBitset features)
36703735
{
3736+
// ConfidentialMPTConvert
36713737
testConvert(features);
36723738
testConvertPreflight(features);
36733739
testConvertPreclaim(features);
36743740
testConvertWithAuditor(features);
36753741

3742+
// ConfidentialMPTMergeInbox
36763743
testMergeInbox(features);
36773744
testMergeInboxPreflight(features);
36783745
testMergeInboxPreclaim(features);
@@ -3683,6 +3750,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
36833750
testSend(features);
36843751
testSendPreflight(features);
36853752
testSendPreclaim(features);
3753+
testSendRangeProof(features);
36863754
testSendDepositPreauth(features);
36873755
testSendWithAuditor(features);
36883756

@@ -3695,6 +3763,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
36953763

36963764
testDelete(features);
36973765

3766+
// ConfidentialMPTConvertBack
36983767
testConvertBack(features);
36993768
testConvertBackPreflight(features);
37003769
testConvertBackPreclaim(features);

src/test/jtx/impl/mpt.cpp

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -820,10 +820,27 @@ MPTTester::getConfidentialSendProof(
820820

821821
auto const balanceLinkageProof = getBalanceLinkageProof(sender, contextHash, *senderPubKey, balanceParams);
822822

823+
std::uint64_t const remainingBalance = balanceParams.amt - amount;
824+
825+
// Compute the blinding factor for the remaining balance: rho_rem = rho_balance - rho_amount
826+
unsigned char rho_rem[32];
827+
unsigned char neg_rho_m[32];
828+
829+
secp256k1_mpt_scalar_negate(neg_rho_m, amountParams.blindingFactor.data());
830+
secp256k1_mpt_scalar_add(rho_rem, balanceParams.blindingFactor.data(), neg_rho_m);
831+
832+
// Generate bulletproof for the amount and remaining balance
833+
Buffer const bulletproof =
834+
getBulletproof({amount, remainingBalance}, {amountParams.blindingFactor, Buffer(rho_rem, 32)}, contextHash);
835+
836+
OPENSSL_cleanse(neg_rho_m, 32);
837+
OPENSSL_cleanse(rho_rem, 32);
838+
823839
auto const sizeAmountLinkage = amountLinkageProof.size();
824840
auto const sizeBalanceLinkage = balanceLinkageProof.size();
841+
auto const sizeBulletproof = bulletproof.size();
825842

826-
size_t const proofSize = sizeEquality + sizeAmountLinkage + sizeBalanceLinkage;
843+
size_t const proofSize = sizeEquality + sizeAmountLinkage + sizeBalanceLinkage + sizeBulletproof;
827844
Buffer proof(proofSize);
828845

829846
auto ptr = proof.data();
@@ -834,6 +851,9 @@ MPTTester::getConfidentialSendProof(
834851
ptr += sizeAmountLinkage;
835852

836853
std::memcpy(ptr, balanceLinkageProof.data(), sizeBalanceLinkage);
854+
ptr += sizeBalanceLinkage;
855+
856+
std::memcpy(ptr, bulletproof.data(), sizeBulletproof);
837857

838858
return proof;
839859
}
@@ -1298,7 +1318,8 @@ MPTTester::send(MPTConfidentialSend const& arg)
12981318
jv[sfZKProof.jsonName] = strHex(*proof);
12991319
else
13001320
{
1301-
size_t const dummySize = secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients);
1321+
size_t const sizeEquality = secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients);
1322+
size_t const dummySize = sizeEquality + 2 * ecPedersenProofLength + ecDoubleBulletproofLength;
13021323

13031324
jv[sfZKProof.jsonName] = strHex(Buffer(dummySize));
13041325
}

src/xrpld/app/tx/detail/ConfidentialMPTSend.cpp

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ ConfidentialMPTSend::preflight(PreflightContext const& ctx)
4444
auto const sizeEquality = getMultiCiphertextEqualityProofSize(recipientCount);
4545
auto const sizePedersenLinkage = 2 * ecPedersenProofLength;
4646

47-
if (ctx.tx[sfZKProof].length() != sizeEquality + sizePedersenLinkage)
47+
if (ctx.tx[sfZKProof].length() != sizeEquality + sizePedersenLinkage + ecDoubleBulletproofLength)
4848
return temMALFORMED;
4949

5050
// Check the Pedersen commitments are valid
@@ -105,7 +105,14 @@ verifySendProofs(
105105
currentOffset += ecPedersenProofLength;
106106
remainingLength -= ecPedersenProofLength;
107107

108-
// todo: Extract range proof once the lib is ready
108+
// Extract range proof
109+
if (remainingLength < ecDoubleBulletproofLength)
110+
return tecINTERNAL;
111+
112+
auto const rangeProof = proof.substr(currentOffset, ecDoubleBulletproofLength);
113+
currentOffset += ecDoubleBulletproofLength;
114+
remainingLength -= ecDoubleBulletproofLength;
115+
109116
if (remainingLength != 0)
110117
return tecINTERNAL; // LCOV_EXCL_LINE
111118

@@ -114,9 +121,7 @@ verifySendProofs(
114121
recipients.reserve(recipientCount);
115122

116123
recipients.push_back({(*sleSenderMPToken)[sfHolderElGamalPublicKey], ctx.tx[sfSenderEncryptedAmount]});
117-
118124
recipients.push_back({(*sleDestinationMPToken)[sfHolderElGamalPublicKey], ctx.tx[sfDestinationEncryptedAmount]});
119-
120125
recipients.push_back({(*sleIssuance)[sfIssuerElGamalPublicKey], ctx.tx[sfIssuerEncryptedAmount]});
121126

122127
if (hasAuditor)
@@ -132,12 +137,15 @@ verifySendProofs(
132137
ctx.tx[sfDestination],
133138
(*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0));
134139

140+
// Use a boolean flag to track validity instead of returning early on failure to prevent leaking information about
141+
// which proof failed through timing differences
142+
bool valid = true;
143+
135144
// Verify the multi-ciphertext equality proof
136145
if (auto const ter = verifyMultiCiphertextEqualityProof(equalityProof, recipients, recipientCount, contextHash);
137146
!isTesSuccess(ter))
138147
{
139-
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: Equality proof failed.";
140-
return ter;
148+
valid = false;
141149
}
142150

143151
// Verify amount linkage
@@ -149,8 +157,7 @@ verifySendProofs(
149157
contextHash);
150158
!isTesSuccess(ter))
151159
{
152-
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: Amount linkage proof failed.";
153-
return ter;
160+
valid = false;
154161
}
155162

156163
// Verify balance linkage
@@ -162,8 +169,38 @@ verifySendProofs(
162169
contextHash);
163170
!isTesSuccess(ter))
164171
{
165-
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: Balance linkage proof failed.";
166-
return ter;
172+
valid = false;
173+
}
174+
175+
// Verify Range Proof
176+
{
177+
Buffer pcRem;
178+
179+
// Derive PC_rem = PC_balance - PC_amount
180+
if (auto const ter = computeSendRemainder(ctx.tx[sfBalanceCommitment], ctx.tx[sfAmountCommitment], pcRem);
181+
!isTesSuccess(ter))
182+
{
183+
valid = false;
184+
}
185+
else
186+
{
187+
// Aggregated commitments: [PC_amount, PC_rem]
188+
// Prove that both the transfer amount and the remaining balance are in range
189+
std::vector<Slice> commitments;
190+
commitments.push_back(ctx.tx[sfAmountCommitment]);
191+
commitments.push_back(Slice{pcRem.data(), pcRem.size()});
192+
193+
if (auto const ter = verifyAggregatedBulletproof(rangeProof, commitments, contextHash); !isTesSuccess(ter))
194+
{
195+
valid = false;
196+
}
197+
}
198+
}
199+
200+
if (!valid)
201+
{
202+
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: One or more cryptographic proofs failed.";
203+
return tecBAD_PROOF;
167204
}
168205

169206
return tesSUCCESS;

0 commit comments

Comments
 (0)