xrpld
Loading...
Searching...
No Matches
src/test/jtx/ConfidentialTransfer.h
1#pragma once
2
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/mpt.h>
6#include <test/jtx/vault.h>
7
8#include <xrpl/basics/Buffer.h>
9#include <xrpl/basics/Slice.h>
10#include <xrpl/basics/base_uint.h>
11#include <xrpl/basics/contract.h>
12#include <xrpl/basics/strHex.h>
13#include <xrpl/beast/unit_test/suite.h>
14#include <xrpl/protocol/ConfidentialTransfer.h>
15#include <xrpl/protocol/Protocol.h>
16#include <xrpl/protocol/TER.h>
17#include <xrpl/protocol/TxFlags.h>
18
19#include <utility/mpt_utility.h>
20
21#include <secp256k1.h>
22#include <secp256k1_mpt.h>
23
24#include <array>
25#include <cstddef>
26#include <cstdint>
27#include <cstring>
28#include <functional>
29#include <optional>
30#include <stdexcept>
31#include <string>
32#include <vector>
33
34namespace xrpl {
35
37{
38protected:
39 template <class T>
40 static T
41 requireOptional(std::optional<T> value, char const* message)
42 {
43 if (!value)
45 return std::move(*value);
46 }
47
48 template <class T>
49 static T const&
50 requireOptionalRef(std::optional<T> const& value, char const* message)
51 {
52 if (!value)
54 return *value;
55 }
56
57 // Offset where the bulletproof begins in a send proof blob.
58 // Proof layout: [compact_sigma | bulletproof]
60
61 // Generate a forged aggregated bulletproof (double bulletproof) for
62 // the given values and blinding factors. Used to test that splicing
63 // a bulletproof claiming a different remaining balance is rejected.
64 // secp256k1 convention: returns 1 on success, 0 on failure.
65 static Buffer
67 std::array<uint64_t, 2> const& values,
68 std::array<Buffer, 2> const& blindingFactors,
69 uint256 const& contextHash)
70 {
71 auto* const ctx = mpt_secp256k1_context();
72
73 secp256k1_pubkey h;
74 secp256k1_mpt_get_h_generator(ctx, &h);
75
77 size_t proofLen = kEcDoubleBulletproofLength;
78
79 unsigned char blindings[64];
80 std::memcpy(blindings, blindingFactors[0].data(), 32);
81 std::memcpy(blindings + 32, blindingFactors[1].data(), 32);
82
83 if (secp256k1_bulletproof_prove_agg(
84 ctx,
85 proof.data(),
86 &proofLen,
87 values.data(),
88 blindings,
89 2,
90 &h,
91 contextHash.data()) == 0)
92 Throw<std::runtime_error>("Failed to generate forged bulletproof");
93
94 return proof;
95 }
96
97 // Generate a forged single bulletproof for a single value and blinding factor.
98 // Used to test ConvertBack overdraft prevention via bulletproof verification.
99 static Buffer
101 uint64_t value,
102 Buffer const& blindingFactor,
103 uint256 const& contextHash)
104 {
105 auto* const ctx = mpt_secp256k1_context();
106
107 secp256k1_pubkey h;
108 secp256k1_mpt_get_h_generator(ctx, &h);
109
111 size_t proofLen = kEcSingleBulletproofLength;
112
113 if (secp256k1_bulletproof_prove_agg(
114 ctx,
115 proof.data(),
116 &proofLen,
117 &value,
118 blindingFactor.data(),
119 1, // m = 1 (single bulletproof)
120 &h,
121 contextHash.data()) == 0)
122 Throw<std::runtime_error>("Failed to generate forged single bulletproof");
123
124 return proof;
125 }
126
127 // Get a bad ciphertext with valid structure but cryptographic invalid for
128 // testing purposes. For preflight test purposes.
129 static Buffer const&
131 {
132 static Buffer const kBadCiphertext = []() {
135
138 return buf;
139 }();
140
141 return kBadCiphertext;
142 }
143
144 // Get a trivial buffer that is structurally and mathematically valid, but
145 // contains invalid data that does not match the ledger state. For preclaim
146 // test purposes.
147 static Buffer const&
149 {
150 static Buffer const kTrivialCiphertext = []() {
153
156
157 buf.data()[kEcCiphertextComponentLength - 1] = 0x01;
158 buf.data()[kEcGamalEncryptedTotalLength - 1] = 0x01;
159
160 return buf;
161 }();
162
163 return kTrivialCiphertext;
164 }
165
166 // Returns a valid compressed EC point (33 bytes) that can pass preflight
167 // validation but contains invalid data for preclaim test purposes.
168 static Buffer const&
170 {
171 static Buffer const kTrivialCommitment = []() {
174
176 // Set last byte to make it a valid x-coordinate on the curve
177 buf.data()[kEcPedersenCommitmentLength - 1] = 0x01;
178
179 return buf;
180 }();
181
182 return kTrivialCommitment;
183 }
184
185 static std::string
187 {
190
192 {
195 buf.data()[i + kEcCiphertextComponentLength - 1] = 0x01;
196 }
197
198 return strHex(buf);
199 }
200
201 // Helper struct to encapsulate common setup for integration tests.
203 {
204 // Constants
205 uint64_t sendAmount;
207 uint32_t version;
208
209 // Blinding factors
213
214 // Encrypted amounts
219
220 // Commitments
222
223 // Long-lived pub key buffers (to avoid dangling Slice)
228
229 // Balance data
230 uint64_t prevSpending;
232
233 // Balance commitment (declared after prevSpending for init order)
235
236 // Recipients vector
238
239 // Constructor that performs all common setup
242 test::jtx::Account const& sender,
243 test::jtx::Account const& dest,
244 test::jtx::Account const& issuer,
245 uint64_t amount,
247 : sendAmount(amount)
248 , nRecipients(auditor ? 4 : 3)
249 , version(mpt.getMPTokenVersion(sender))
253 , senderAmt(mpt.encryptAmount(sender, amount, blindingFactor))
254 , destAmt(mpt.encryptAmount(dest, amount, blindingFactor))
255 , issuerAmt(mpt.encryptAmount(issuer, amount, blindingFactor))
256 , auditorAmt(
257 auditor ? std::optional<Buffer>(
258 mpt.encryptAmount(auditor->get(), amount, blindingFactor))
259 : std::nullopt)
260 , amountCommitment(mpt.getPedersenCommitment(amount, amountBlindingFactor))
261 , senderPubKey(requireOptional(mpt.getPubKey(sender), "Missing sender public key"))
262 , destPubKey(requireOptional(mpt.getPubKey(dest), "Missing destination public key"))
263 , issuerPubKey(requireOptional(mpt.getPubKey(issuer), "Missing issuer public key"))
264 , auditorPubKey(auditor ? mpt.getPubKey(auditor->get()) : std::nullopt)
266 mpt.getDecryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
267 "Missing sender spending balance"))
269 mpt.getEncryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
270 "Missing sender encrypted spending balance"))
271 , balanceCommitment(mpt.getPedersenCommitment(prevSpending, balanceBlindingFactor))
272 {
273 recipients.push_back({
274 .publicKey = Slice(senderPubKey),
275 .encryptedAmount = senderAmt,
276 });
277 recipients.push_back({
278 .publicKey = Slice(destPubKey),
279 .encryptedAmount = destAmt,
280 });
281 recipients.push_back({
282 .publicKey = Slice(issuerPubKey),
283 .encryptedAmount = issuerAmt,
284 });
285 if (auditor)
286 {
287 recipients.push_back({
288 .publicKey =
289 Slice(requireOptionalRef(auditorPubKey, "Missing auditor public key")),
290 .encryptedAmount =
291 requireOptionalRef(auditorAmt, "Missing auditor encrypted amount"),
292 });
293 }
294 }
295
296 // Generate proof with current account sequence
300 test::jtx::Env& env,
301 test::jtx::Account const& sender,
302 test::jtx::Account const& dest) const
303 {
304 auto const ctxHash = getSendContextHash(
305 sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), version);
306
307 return mpt.getConfidentialSendProof(
308 sender,
312 ctxHash,
313 {
314 .pedersenCommitment = amountCommitment,
315 .amt = sendAmount,
316 .encryptedAmt = senderAmt,
317 .blindingFactor = amountBlindingFactor,
318 },
319 {
320 .pedersenCommitment = balanceCommitment,
321 .amt = prevSpending,
322 .encryptedAmt = prevEncryptedSpending,
323 .blindingFactor = balanceBlindingFactor,
324 });
325 }
326
329 test::jtx::Account const& sender,
330 test::jtx::Account const& dest,
331 Buffer const& proof,
332 std::optional<TER> err = std::nullopt) const
333 {
334 return {
335 .account = sender,
336 .dest = dest,
337 .amt = sendAmount,
338 .proof = strHex(proof),
339 .senderEncryptedAmt = senderAmt,
340 .destEncryptedAmt = destAmt,
341 .issuerEncryptedAmt = issuerAmt,
342 .auditorEncryptedAmt = auditorAmt,
343 .amountCommitment = amountCommitment,
344 .balanceCommitment = balanceCommitment,
345 .err = err,
346 };
347 }
348 };
349
350 // Helper that wraps the boilerplate setup: Env + MPT creation, funding, key
351 // generation, and seeding each holder with a confidential balance.
352 // The caller supplies the issuer and any number of holders.
354 {
355 // Per-holder configuration: the account, how much MPT to fund it
356 // with, and how much of that to convert to a confidential balance.
363
365
367 test::jtx::Env& env,
368 test::jtx::Account const& issuer,
369 std::vector<HolderInit> const& holders,
370 std::uint32_t flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
371 std::optional<test::jtx::Account> auditor = std::nullopt)
372 : mpt{env, issuer, {.holders = extractAccounts(holders), .auditor = auditor}}
373 {
374 mpt.create({.ownerCount = 1, .flags = flags});
375
376 for (auto const& h : holders)
377 {
378 mpt.authorize({.account = h.account});
379 if ((flags & tfMPTRequireAuth) != 0)
380 mpt.authorize({.account = issuer, .holder = h.account});
381 mpt.pay(issuer, h.account, h.payAmount);
382 }
383
384 mpt.generateKeyPair(issuer);
385 for (auto const& h : holders)
386 mpt.generateKeyPair(h.account);
387 if (auditor)
388 mpt.generateKeyPair(requireOptionalRef(auditor, "Missing auditor"));
389
390 mpt.set({
391 .account = issuer,
392 .issuerPubKey = mpt.getPubKey(issuer),
393 .auditorPubKey = auditor
394 ? mpt.getPubKey(requireOptionalRef(auditor, "Missing auditor"))
395 : std::optional<Buffer>{},
396 });
397
398 for (auto const& h : holders)
399 {
400 mpt.convert({
401 .account = h.account,
402 .amt = h.convertAmount,
403 .holderPubKey = mpt.getPubKey(h.account),
404 });
405 mpt.mergeInbox({.account = h.account});
406 }
407 }
408
409 private:
410 static std::vector<test::jtx::Account>
412 {
414 accounts.reserve(holders.size());
415 for (auto const& h : holders)
416 accounts.push_back(h.account);
417 return accounts;
418 }
419 };
420
421 // Set up an MPT environment suitable for batch testing.
422 // alice is issuer; bob has 'bobAmt' in confidential spending; carol has
423 // 'carolAmt' in confidential spending; dave is initialised with pubkey but
424 // zero spending/inbox.
425 static void
428 test::jtx::Account const& alice,
429 test::jtx::Account const& bob,
430 test::jtx::Account const& carol,
431 test::jtx::Account const& dave,
432 std::uint64_t bobAmt,
433 std::uint64_t carolAmt)
434 {
435 using namespace test::jtx;
436 mpt.create({
437 .ownerCount = 1,
438 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
439 });
440 mpt.authorize({.account = bob});
441 mpt.authorize({.account = carol});
442 mpt.authorize({.account = dave});
443
444 if (bobAmt > 0)
445 mpt.pay(alice, bob, bobAmt);
446 if (carolAmt > 0)
447 mpt.pay(alice, carol, carolAmt);
448
449 mpt.generateKeyPair(alice);
450 mpt.generateKeyPair(bob);
451 mpt.generateKeyPair(carol);
452 mpt.generateKeyPair(dave);
453
454 mpt.set({
455 .account = alice,
456 .issuerPubKey = mpt.getPubKey(alice),
457 });
458
459 if (bobAmt > 0)
460 {
461 mpt.convert({
462 .account = bob,
463 .amt = bobAmt,
464 .holderPubKey = mpt.getPubKey(bob),
465 });
466 mpt.mergeInbox({.account = bob});
467 }
468 else
469 {
470 mpt.convert({
471 .account = bob,
472 .amt = 0,
473 .holderPubKey = mpt.getPubKey(bob),
474 });
475 }
476
477 if (carolAmt > 0)
478 {
479 mpt.convert({
480 .account = carol,
481 .amt = carolAmt,
482 .holderPubKey = mpt.getPubKey(carol),
483 });
484 mpt.mergeInbox({.account = carol});
485 }
486 else
487 {
488 mpt.convert({
489 .account = carol,
490 .amt = 0,
491 .holderPubKey = mpt.getPubKey(carol),
492 });
493 }
494
495 // dave: register pubkey only (0 spending/inbox)
496 mpt.convert({
497 .account = dave,
498 .amt = 0,
499 .holderPubKey = mpt.getPubKey(dave),
500 });
501 }
502};
503
504} // namespace xrpl
A testsuite class.
Definition suite.h:52
pointer data()
Definition base_uint.h:117
Like std::vector<char> but better.
Definition Buffer.h:19
std::uint8_t const * data() const noexcept
Return a pointer to beginning of the storage.
Definition Buffer.h:148
static T requireOptional(std::optional< T > value, char const *message)
static Buffer getForgedSingleBulletproof(uint64_t value, Buffer const &blindingFactor, uint256 const &contextHash)
static T const & requireOptionalRef(std::optional< T > const &value, char const *message)
static void setupBatchEnv(test::jtx::MPTTester &mpt, test::jtx::Account const &alice, test::jtx::Account const &bob, test::jtx::Account const &carol, test::jtx::Account const &dave, std::uint64_t bobAmt, std::uint64_t carolAmt)
static Buffer getForgedBulletproof(std::array< uint64_t, 2 > const &values, std::array< Buffer, 2 > const &blindingFactors, uint256 const &contextHash)
An immutable linear range of bytes.
Definition Slice.h:28
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:447
void set(MPTSet const &set={})
Definition mpt.cpp:467
void pay(Account const &src, Account const &dest, std::int64_t amount, std::optional< TER > err=std::nullopt, std::optional< std::vector< std::string > > credentials=std::nullopt)
Definition mpt.cpp:652
void create(MPTCreate const &arg=MPTCreate{})
Definition mpt.cpp:241
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:353
MPTID const & issuanceID() const
Definition mpt.h:641
std::optional< Buffer > getConfidentialSendProof(Account const &sender, std::uint64_t const amount, std::vector< ConfidentialRecipient > const &recipients, Slice const &blindingFactor, uint256 const &contextHash, PedersenProofParams const &amountParams, PedersenProofParams const &balanceParams) const
Definition mpt.cpp:834
void generateKeyPair(Account const &account)
Definition mpt.cpp:2011
void mergeInbox(MPTMergeInbox const &arg=MPTMergeInbox{})
Definition mpt.cpp:2148
void convert(MPTConvert const &arg=MPTConvert{})
Definition mpt.cpp:1070
std::optional< Buffer > getPubKey(Account const &account) const
Definition mpt.cpp:2033
T data(T... args)
T memcpy(T... args)
T memset(T... args)
STL namespace.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
constexpr std::uint8_t kEcCompressedPrefixEvenY
Compressed EC point prefix for even y-coordinate.
Definition Protocol.h:539
T get(Section const &section, std::string const &name, T const &defaultValue=T{})
Retrieve a key/value pair from a section.
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:13
std::optional< Buffer > encryptAmount(uint64_t const amt, Slice const &pubKeySlice, Slice const &blindingFactor)
Encrypts an amount using ElGamal encryption.
constexpr std::size_t kEcGamalEncryptedTotalLength
EC ElGamal ciphertext length: two compressed EC points concatenated.
Definition Protocol.h:468
constexpr std::size_t kEcSingleBulletproofLength
Length of single bulletproof (range proof for 1 commitment) in bytes.
Definition Protocol.h:498
constexpr std::size_t kEcPedersenCommitmentLength
Length of Pedersen Commitment (compressed).
Definition Protocol.h:493
constexpr std::size_t kEcCiphertextComponentLength
Length of one compressed EC point component in an EC ElGamal ciphertext.
Definition Protocol.h:463
constexpr std::size_t kEcDoubleBulletproofLength
Length of double bulletproof (range proof for 2 commitments) in bytes.
Definition Protocol.h:503
Buffer generateBlindingFactor()
Generates a cryptographically secure blinding factor (size=xrpl::kEcBlindingFactorLength).
uint256 getSendContextHash(AccountID const &account, uint192 const &issuanceID, std::uint32_t sequence, AccountID const &destination, std::uint32_t version)
Generates the context hash for ConfidentialMPTSend transactions.
constexpr std::size_t kEcSendProofLength
192 bytes compact sigma proof + 754 bytes double bulletproof.
Definition Protocol.h:513
BaseUInt< 256 > uint256
Definition base_uint.h:580
XRPL_NO_SANITIZE_ADDRESS void Throw(Args &&... args)
Definition contract.h:52
T push_back(T... args)
T reserve(T... args)
T size(T... args)
static std::vector< test::jtx::Account > extractAccounts(std::vector< HolderInit > const &holders)
ConfidentialEnv(test::jtx::Env &env, test::jtx::Account const &issuer, std::vector< HolderInit > const &holders, std::uint32_t flags=tfMPTCanLock|tfMPTCanHoldConfidentialBalance|tfMPTCanTransfer, std::optional< test::jtx::Account > auditor=std::nullopt)
std::optional< Buffer > generateProof(test::jtx::MPTTester &mpt, test::jtx::Env &env, test::jtx::Account const &sender, test::jtx::Account const &dest) const
test::jtx::MPTConfidentialSend sendArgs(test::jtx::Account const &sender, test::jtx::Account const &dest, Buffer const &proof, std::optional< TER > err=std::nullopt) const
ConfidentialSendSetup(test::jtx::MPTTester &mpt, test::jtx::Account const &sender, test::jtx::Account const &dest, test::jtx::Account const &issuer, uint64_t amount, std::optional< std::reference_wrapper< test::jtx::Account const > > auditor=std::nullopt)
Arguments for building a ConfidentialMPTSend test transaction.
Definition mpt.h:293