xrpld
Loading...
Searching...
No Matches
ConfidentialMPTSend.cpp
1#include <xrpl/tx/transactors/token/ConfidentialMPTSend.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Slice.h>
5#include <xrpl/core/ServiceRegistry.h>
6#include <xrpl/ledger/ReadView.h>
7#include <xrpl/ledger/helpers/CredentialHelpers.h>
8#include <xrpl/ledger/helpers/TokenHelpers.h>
9#include <xrpl/protocol/ConfidentialTransfer.h>
10#include <xrpl/protocol/Feature.h>
11#include <xrpl/protocol/Indexes.h>
12#include <xrpl/protocol/LedgerFormats.h>
13#include <xrpl/protocol/Protocol.h>
14#include <xrpl/protocol/SField.h>
15#include <xrpl/protocol/TER.h>
16#include <xrpl/protocol/XRPAmount.h>
17#include <xrpl/tx/Transactor.h>
18
19#include <memory>
20#include <optional>
21#include <utility>
22
23namespace xrpl {
24
25bool
27{
28 return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
29}
30
33{
34 if (!ctx.rules.enabled(featureConfidentialTransfer))
35 return temDISABLED;
36
37 auto const account = ctx.tx[sfAccount];
38 auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer();
39
40 // ConfidentialMPTSend only allows holder to holder, holder to second account,
41 // and second account to holder transfers. So issuer cannot be the sender.
42 if (account == issuer)
43 return temMALFORMED;
44
45 // Can not send to self
46 if (account == ctx.tx[sfDestination])
47 return temMALFORMED;
48
49 // Issuer cannot be the destination
50 if (ctx.tx[sfDestination] == issuer)
51 return temMALFORMED;
52
53 // Check the length of the encrypted amounts
54 if (ctx.tx[sfSenderEncryptedAmount].length() != kEcGamalEncryptedTotalLength ||
55 ctx.tx[sfDestinationEncryptedAmount].length() != kEcGamalEncryptedTotalLength ||
56 ctx.tx[sfIssuerEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
57 {
58 return temBAD_CIPHERTEXT;
59 }
60
61 bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
62 if (hasAuditor && ctx.tx[sfAuditorEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
63 return temBAD_CIPHERTEXT;
64
65 // Check the length of the ZKProof (fixed size regardless of recipient count)
66 if (ctx.tx[sfZKProof].length() != kEcSendProofLength)
67 return temMALFORMED;
68
69 // Check the Pedersen commitments are valid
70 if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]) ||
71 !isValidCompressedECPoint(ctx.tx[sfAmountCommitment]))
72 {
73 return temMALFORMED;
74 }
75
76 // Check the encrypted amount formats, this is more expensive so put it at
77 // the end
78 if (!isValidCiphertext(ctx.tx[sfSenderEncryptedAmount]) ||
79 !isValidCiphertext(ctx.tx[sfDestinationEncryptedAmount]) ||
80 !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
81 {
82 return temBAD_CIPHERTEXT;
83 }
84
85 if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount]))
86 return temBAD_CIPHERTEXT;
87
88 if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
89 return err;
90
91 return tesSUCCESS;
92}
93
99
100namespace detail {
101
102static TER
104 PreclaimContext const& ctx,
105 std::shared_ptr<SLE const> const& sleSenderMPToken,
106 std::shared_ptr<SLE const> const& sleDestinationMPToken,
107 std::shared_ptr<SLE const> const& sleIssuance)
108{
109 // Sanity check
110 if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance)
111 return tecINTERNAL; // LCOV_EXCL_LINE
112
113 auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
114
116 if (hasAuditor)
117 {
118 auditor.emplace(
120 .publicKey = (*sleIssuance)[sfAuditorEncryptionKey],
121 .encryptedAmount = ctx.tx[sfAuditorEncryptedAmount],
122 });
123 }
124
125 auto const contextHash = getSendContextHash(
126 ctx.tx[sfAccount],
127 ctx.tx[sfMPTokenIssuanceID],
128 ctx.tx.getSeqProxy().value(),
129 ctx.tx[sfDestination],
130 (*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0));
131
132 return verifySendProof(
133 ctx.tx[sfZKProof],
134 {
135 .publicKey = (*sleSenderMPToken)[sfHolderEncryptionKey],
136 .encryptedAmount = ctx.tx[sfSenderEncryptedAmount],
137 },
138 {
139 .publicKey = (*sleDestinationMPToken)[sfHolderEncryptionKey],
140 .encryptedAmount = ctx.tx[sfDestinationEncryptedAmount],
141 },
142 {
143 .publicKey = (*sleIssuance)[sfIssuerEncryptionKey],
144 .encryptedAmount = ctx.tx[sfIssuerEncryptedAmount],
145 },
146 auditor,
147 (*sleSenderMPToken)[sfConfidentialBalanceSpending],
148 ctx.tx[sfAmountCommitment],
149 ctx.tx[sfBalanceCommitment],
150 contextHash);
151}
152
153} // namespace detail
154
155TER
157{
158 // Check if sender account exists
159 auto const account = ctx.tx[sfAccount];
160 if (!ctx.view.exists(keylet::account(account)))
161 return terNO_ACCOUNT;
162
163 // Check if destination account exists
164 auto const destination = ctx.tx[sfDestination];
165 auto const sleDst = ctx.view.read(keylet::account(destination));
166 if (!sleDst)
167 return tecNO_TARGET;
168
169 // Check destination tag
170 if (((sleDst->getFlags() & lsfRequireDestTag) != 0u) &&
171 !ctx.tx.isFieldPresent(sfDestinationTag))
172 {
173 return tecDST_TAG_NEEDED;
174 }
175
176 // Check if MPT issuance exists
177 auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
178 auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
179 if (!sleIssuance)
180 return tecOBJECT_NOT_FOUND;
181
182 // Check if the issuance allows transfer
183 if (!sleIssuance->isFlag(lsfMPTCanTransfer))
184 return tecNO_AUTH;
185
186 // Check if issuance allows confidential transfer
187 if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
188 return tecNO_PERMISSION;
189
190 // Sanity check: transfer fee must be 0 for confidential MPTs. This should
191 // be unreachable in valid ledger state because MPTokenIssuanceCreate and
192 // MPTokenIssuanceSet enforce it.
193 if ((*sleIssuance)[~sfTransferFee].value_or(0) > 0)
194 return tecNO_PERMISSION;
195
196 // Check if issuance has issuer ElGamal public key
197 if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
198 return tecNO_PERMISSION;
199
200 bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
201 bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
202
203 // Tx must include auditor ciphertext if the issuance has enabled
204 // auditing, and must not include it if auditing is not enabled
205 if (requiresAuditor != hasAuditor)
206 return tecNO_PERMISSION;
207
208 // Sanity check: issuer isn't the sender
209 if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
210 return tefINTERNAL; // LCOV_EXCL_LINE
211
212 // Check sender's MPToken existence
213 auto const sleSenderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
214 if (!sleSenderMPToken)
215 return tecOBJECT_NOT_FOUND;
216
217 // Check sender's MPToken has necessary fields for confidential send
218 if (!sleSenderMPToken->isFieldPresent(sfHolderEncryptionKey) ||
219 !sleSenderMPToken->isFieldPresent(sfConfidentialBalanceSpending) ||
220 !sleSenderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
221 {
222 return tecNO_PERMISSION;
223 }
224
225 // Check destination's MPToken existence
226 auto const sleDestinationMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, destination));
227 if (!sleDestinationMPToken)
228 return tecOBJECT_NOT_FOUND;
229
230 // Check destination's MPToken has necessary fields for confidential send
231 if (!sleDestinationMPToken->isFieldPresent(sfHolderEncryptionKey) ||
232 !sleDestinationMPToken->isFieldPresent(sfConfidentialBalanceInbox) ||
233 !sleDestinationMPToken->isFieldPresent(sfIssuerEncryptedBalance))
234 {
235 return tecNO_PERMISSION;
236 }
237
238 // Sanity check: Both MPTokens' auditor fields must be present if auditing
239 // is enabled
240 if (requiresAuditor &&
241 (!sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance) ||
242 !sleDestinationMPToken->isFieldPresent(sfAuditorEncryptedBalance)))
243 {
244 return tefINTERNAL; // LCOV_EXCL_LINE
245 }
246
247 // Check lock
248 MPTIssue const mptIssue(mptIssuanceID);
249 if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
250 return ter;
251
252 if (auto const ter = checkFrozen(ctx.view, destination, mptIssue); !isTesSuccess(ter))
253 return ter;
254
255 // Check auth
256 if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
257 return ter;
258
259 if (auto const ter = requireAuth(ctx.view, mptIssue, destination); !isTesSuccess(ter))
260 return ter;
261
262 if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
263 !isTesSuccess(err))
264 return err;
265
266 // Check deposit preauth before the expensive ZK proof verification.
267 // Uses read-only view.
268 auto const preauthErr =
269 checkDepositPreauth(ctx.tx, ctx.view, account, destination, sleDst, ctx.j);
270 if (!isTesSuccess(preauthErr))
271 return preauthErr;
272
273 return detail::verifySendProofs(ctx, sleSenderMPToken, sleDestinationMPToken, sleIssuance);
274}
275
276TER
278{
279 auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
280 auto const destination = ctx_.tx[sfDestination];
281
282 auto sleSenderMPToken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
283 auto sleDestinationMPToken = view().peek(keylet::mptoken(mptIssuanceID, destination));
284 auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID));
285
286 auto const sleDestAcct = view().read(keylet::account(destination));
287
288 if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance || !sleDestAcct)
289 return tecINTERNAL; // LCOV_EXCL_LINE
290
291 // Deposit preauth authorization was already verified in preclaim.
292 // Remove any expired credentials.
293 if (auto err = cleanupExpiredCredentials(ctx_.tx, ctx_.view(), ctx_.journal);
294 !isTesSuccess(err))
295 return err;
296
297 auto const senderEc = ctx_.tx[sfSenderEncryptedAmount];
298 auto const destEc = ctx_.tx[sfDestinationEncryptedAmount];
299 auto const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
300 auto const proof = ctx_.tx[sfZKProof];
301 Slice const sendChallenge{proof.data(), kEcBlindingFactorLength};
302
303 auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
304
305 // Subtract from sender's spending balance
306 {
307 auto const curSpending = (*sleSenderMPToken)[sfConfidentialBalanceSpending];
308 auto newSpending = homomorphicSubtract(curSpending, senderEc);
309 if (!newSpending)
310 {
311 // LCOV_EXCL_START
312 JLOG(ctx_.journal.error())
313 << "ConfidentialMPTSend failed homomorphic subtract for sender spending balance.";
314 return tecINTERNAL;
315 // LCOV_EXCL_STOP
316 }
317
318 (*sleSenderMPToken)[sfConfidentialBalanceSpending] = std::move(*newSpending);
319 }
320
321 // Subtract from issuer's balance
322 {
323 auto const curIssuerEnc = (*sleSenderMPToken)[sfIssuerEncryptedBalance];
324 auto newIssuerEnc = homomorphicSubtract(curIssuerEnc, issuerEc);
325 if (!newIssuerEnc)
326 {
327 // LCOV_EXCL_START
328 JLOG(ctx_.journal.error())
329 << "ConfidentialMPTSend failed homomorphic subtract for sender issuer balance.";
330 return tecINTERNAL;
331 // LCOV_EXCL_STOP
332 }
333
334 (*sleSenderMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc);
335 }
336
337 // Subtract from auditor's balance if present
338 if (auditorEc)
339 {
340 auto const curAuditorEnc = (*sleSenderMPToken)[sfAuditorEncryptedBalance];
341 auto newAuditorEnc = homomorphicSubtract(curAuditorEnc, *auditorEc);
342 if (!newAuditorEnc)
343 {
344 // LCOV_EXCL_START
345 JLOG(ctx_.journal.error())
346 << "ConfidentialMPTSend failed homomorphic subtract for sender auditor balance.";
347 return tecINTERNAL;
348 // LCOV_EXCL_STOP
349 }
350
351 (*sleSenderMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc);
352 }
353
354 // Add to destination's inbox balance
355 {
356 auto rerandomizedDestEc = rerandomizeCiphertext(
357 destEc, (*sleDestinationMPToken)[sfHolderEncryptionKey], sendChallenge);
358 if (!rerandomizedDestEc)
359 return tecINTERNAL; // LCOV_EXCL_LINE
360
361 auto const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox];
362 auto newInbox = homomorphicAdd(curInbox, *rerandomizedDestEc);
363 if (!newInbox)
364 {
365 // LCOV_EXCL_START
366 JLOG(ctx_.journal.error())
367 << "ConfidentialMPTSend failed homomorphic add for destination inbox.";
368 return tecINTERNAL;
369 // LCOV_EXCL_STOP
370 }
371
372 (*sleDestinationMPToken)[sfConfidentialBalanceInbox] = std::move(*newInbox);
373 }
374
375 // Add to issuer's balance
376 {
377 auto rerandomizedIssuerEc =
378 rerandomizeCiphertext(issuerEc, (*sleIssuance)[sfIssuerEncryptionKey], sendChallenge);
379 if (!rerandomizedIssuerEc)
380 return tecINTERNAL; // LCOV_EXCL_LINE
381
382 auto const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance];
383 auto newIssuerEnc = homomorphicAdd(curIssuerEnc, *rerandomizedIssuerEc);
384 if (!newIssuerEnc)
385 {
386 // LCOV_EXCL_START
387 JLOG(ctx_.journal.error())
388 << "ConfidentialMPTSend failed homomorphic add for destination issuer balance.";
389 return tecINTERNAL;
390 // LCOV_EXCL_STOP
391 }
392
393 (*sleDestinationMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc);
394 }
395
396 // Add to auditor's balance if present
397 if (auditorEc)
398 {
399 auto rerandomizedAuditorEc = rerandomizeCiphertext(
400 *auditorEc, (*sleIssuance)[sfAuditorEncryptionKey], sendChallenge);
401 if (!rerandomizedAuditorEc)
402 return tecINTERNAL; // LCOV_EXCL_LINE
403
404 auto const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance];
405 auto newAuditorEnc = homomorphicAdd(curAuditorEnc, *rerandomizedAuditorEc);
406 if (!newAuditorEnc)
407 {
408 // LCOV_EXCL_START
409 JLOG(ctx_.journal.error())
410 << "ConfidentialMPTSend failed homomorphic add for destination auditor balance.";
411 return tecINTERNAL;
412 // LCOV_EXCL_STOP
413 }
414
415 (*sleDestinationMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc);
416 }
417
418 // increment sender version only; receiver version is not modified by incoming sends
419 incrementConfidentialVersion(*sleSenderMPToken);
420
421 view().update(sleSenderMPToken);
422 view().update(sleDestinationMPToken);
423 return tesSUCCESS;
424}
425
426void
433
434bool
436 STTx const&,
437 TER,
438 XRPAmount,
439 ReadView const&,
440 beast::Journal const&)
441{
442 return true;
443}
444
445} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:38
virtual SLE::pointer peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
virtual void update(SLE::ref sle)=0
Indicate changes to a peeked SLE.
void visitInvariantEntry(bool isDelete, std::shared_ptr< SLE const > const &before, std::shared_ptr< SLE const > const &after) override
static TER preclaim(PreclaimContext const &ctx)
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
static bool checkExtraFeatures(PreflightContext const &ctx)
bool finalizeInvariants(STTx const &tx, TER result, XRPAmount fee, ReadView const &view, beast::Journal const &j) override
Check transaction-specific post-conditions after all entries have been visited.
static NotTEC preflight(PreflightContext const &ctx)
AccountID const & getIssuer() const
Definition MPTIssue.cpp:29
A view into a ledger.
Definition ReadView.h:31
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:171
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:454
SeqProxy getSeqProxy() const
Definition STTx.cpp:193
constexpr std::uint32_t value() const
Definition SeqProxy.h:62
An immutable linear range of bytes.
Definition Slice.h:26
ApplyView & view()
Definition Transactor.h:136
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
AccountID const accountID_
Definition Transactor.h:120
ApplyContext & ctx_
Definition Transactor.h:116
T emplace(T... args)
NotTEC checkFields(STTx const &tx, beast::Journal j)
TER valid(STTx const &tx, ReadView const &view, AccountID const &src, beast::Journal j)
static TER verifySendProofs(PreclaimContext const &ctx, std::shared_ptr< SLE const > const &sleSenderMPToken, std::shared_ptr< SLE const > const &sleDestinationMPToken, std::shared_ptr< SLE const > const &sleIssuance)
Keylet mptokenIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Definition Indexes.cpp:521
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:533
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terNO_ACCOUNT
Definition TER.h:209
std::optional< Buffer > rerandomizeCiphertext(Slice const &ciphertext, Slice const &pubKeySlice, Slice const &randomness)
Re-randomizes an ElGamal ciphertext without changing its plaintext.
constexpr std::size_t kEcBlindingFactorLength
Length of the EC blinding factor in bytes.
Definition Protocol.h:333
constexpr std::uint32_t kConfidentialFeeMultiplier
Extra base fee multiplier charged to confidential MPT transactions.
Definition Protocol.h:364
TER checkFrozen(ReadView const &view, AccountID const &account, Issue const &issue)
@ tefINTERNAL
Definition TER.h:163
bool isValidCompressedECPoint(Slice const &buffer)
Verifies that a buffer contains a valid, parsable compressed EC point.
constexpr std::size_t kEcGamalEncryptedTotalLength
EC ElGamal ciphertext length: two compressed EC points concatenated.
Definition Protocol.h:324
bool isValidCiphertext(Slice const &buffer)
Verifies that a buffer contains two valid, parsable EC public keys.
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:594
std::optional< Buffer > homomorphicSubtract(Slice const &a, Slice const &b)
Homomorphically subtracts two ElGamal ciphertexts.
TER cleanupExpiredCredentials(STTx const &tx, ApplyView &view, beast::Journal j)
Remove expired credentials referenced by the transaction.
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:351
@ temBAD_CIPHERTEXT
Definition TER.h:131
@ temMALFORMED
Definition TER.h:73
@ temDISABLED
Definition TER.h:100
bool isTesSuccess(TER x) noexcept
Definition TER.h:663
TERSubset< CanCvtToTER > TER
Definition TER.h:634
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, std::uint8_t depth=0)
Check if the account lacks required authorization for MPT.
@ tecNO_TARGET
Definition TER.h:302
@ tecOBJECT_NOT_FOUND
Definition TER.h:324
@ tecNO_AUTH
Definition TER.h:298
@ tecINTERNAL
Definition TER.h:308
@ tecNO_PERMISSION
Definition TER.h:303
@ tecDST_TAG_NEEDED
Definition TER.h:307
void incrementConfidentialVersion(STObject &mptoken)
Increments the confidential balance version counter on an MPToken.
std::optional< Buffer > homomorphicAdd(Slice const &a, Slice const &b)
Homomorphically adds two ElGamal ciphertexts.
@ tesSUCCESS
Definition TER.h:240
TER checkDepositPreauth(STTx const &tx, ReadView const &view, AccountID const &src, AccountID const &dst, std::shared_ptr< SLE const > const &sleDst, beast::Journal j)
Check whether src is authorized to deposit to dst.
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)
Verifies all zero-knowledge proofs for a ConfidentialMPTSend transaction.
Bundles an ElGamal public key with its associated encrypted amount.
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:61
ReadView const & view
Definition Transactor.h:64
beast::Journal const j
Definition Transactor.h:69
State information when preflighting a tx.
Definition Transactor.h:18
beast::Journal const j
Definition Transactor.h:25