xrpld
Loading...
Searching...
No Matches
ConfidentialMPTConvert.cpp
1#include <xrpl/tx/transactors/token/ConfidentialMPTConvert.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Slice.h>
5#include <xrpl/beast/utility/Journal.h>
6#include <xrpl/beast/utility/instrumentation.h>
7#include <xrpl/core/ServiceRegistry.h>
8#include <xrpl/ledger/ReadView.h>
9#include <xrpl/ledger/helpers/TokenHelpers.h>
10#include <xrpl/protocol/ConfidentialTransfer.h>
11#include <xrpl/protocol/Feature.h>
12#include <xrpl/protocol/Indexes.h>
13#include <xrpl/protocol/LedgerFormats.h>
14#include <xrpl/protocol/MPTIssue.h>
15#include <xrpl/protocol/Protocol.h>
16#include <xrpl/protocol/SField.h>
17#include <xrpl/protocol/TER.h>
18#include <xrpl/protocol/XRPAmount.h>
19#include <xrpl/tx/Transactor.h>
20
21#include <memory>
22#include <optional>
23#include <utility>
24
25namespace xrpl {
26
29{
30 // issuer cannot convert
31 if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
32 return temMALFORMED;
33
34 if (ctx.tx[sfMPTAmount] > kMaxMpTokenAmount)
35 return temBAD_AMOUNT;
36
37 if (ctx.tx.isFieldPresent(sfHolderEncryptionKey))
38 {
39 if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
40 return temMALFORMED;
41
42 // proof of knowledge of the secret key corresponding to the provided
43 // public key is needed when holder ec public key is being set.
44 if (!ctx.tx.isFieldPresent(sfZKProof))
45 return temMALFORMED;
46
47 // verify schnorr proof length when registering holder ec public key
48 if (ctx.tx[sfZKProof].size() != kEcSchnorrProofLength)
49 return temMALFORMED;
50 }
51 else
52 {
53 // Either both sfHolderEncryptionKey and sfZKProof should be present, or both should be
54 // absent.
55 if (ctx.tx.isFieldPresent(sfZKProof))
56 return temMALFORMED;
57 }
58
59 // check encrypted amount format after the above basic checks
60 // this check is more expensive so put it at the end
61 if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
62 return res;
63
64 return tesSUCCESS;
65}
66
72
73TER
75{
76 auto const account = ctx.tx[sfAccount];
77 auto const issuanceID = ctx.tx[sfMPTokenIssuanceID];
78 auto const amount = ctx.tx[sfMPTAmount];
79
80 // ensure that issuance exists
81 auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(issuanceID));
82 if (!sleIssuance)
84
85 if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) ||
86 !sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
87 {
88 return tecNO_PERMISSION;
89 }
90
91 // already checked in preflight, but should also check that issuer on the
92 // issuance isn't the account either
93 if (sleIssuance->getAccountID(sfIssuer) == account)
94 {
95 // LCOV_EXCL_START
96 UNREACHABLE(
97 "xrpl::ConfidentialMPTConvert::preclaim : issuer derived from the MPT ID must "
98 "match the ledger's stored issuer");
99 return tefINTERNAL;
100 // LCOV_EXCL_STOP
101 }
102
103 bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
104 bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
105
106 // tx must include auditor ciphertext if the issuance has enabled
107 // auditing, and must not include it if auditing is not enabled
108 if (requiresAuditor != hasAuditor)
109 return tecNO_PERMISSION;
110
111 auto const sleMptoken = ctx.view.read(keylet::mptoken(issuanceID, account));
112 if (!sleMptoken)
113 return tecOBJECT_NOT_FOUND;
114
115 // An already-initialized holder has their new ciphertexts homomorphically
116 // added to their existing mirrors, so those mirrors must be encrypted under
117 // the currently registered keys. A first-time convert creates the mirrors
118 // under those keys instead, and has nothing to be stale.
119 if (ctx.view.rules().enabled(featureConfidentialMPTKeyRotation) &&
120 sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
121 !areMirrorsCurrent(*sleIssuance, *sleMptoken))
122 {
123 return tecNO_PERMISSION;
124 }
125
126 auto const mptIssue = MPTIssue{issuanceID};
127
128 // Explicit freeze and auth checks are required because accountHolds
129 // with ZeroIfFrozen/ZeroIfUnauthorized only implicitly rejects
130 // non-zero amounts. A zero-amount convert would bypass those implicit
131 // checks, allowing frozen or unauthorized accounts to register ElGamal
132 // keys and initialize confidential balance fields.
133
134 // Check lock
135 if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
136 return ter;
137
138 // Check auth
139 if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
140 return ter;
141
142 auto const mptAmount =
143 STAmount(MPTAmount{static_cast<MPTAmount::value_type>(amount)}, mptIssue);
144 if (accountHolds(
145 ctx.view,
146 account,
147 mptIssue,
150 ctx.j) < mptAmount)
151 {
153 }
154
155 auto const hasHolderKeyOnLedger = sleMptoken->isFieldPresent(sfHolderEncryptionKey);
156 auto const hasHolderKeyInTx = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
157
158 // must have pk to convert
159 if (!hasHolderKeyOnLedger && !hasHolderKeyInTx)
160 return tecNO_PERMISSION;
161
162 // can't update if there's already a pk
163 if (hasHolderKeyOnLedger && hasHolderKeyInTx)
164 return tecDUPLICATE;
165
166 // Run all verifications before returning any error to prevent timing attacks
167 // that could reveal which proof failed.
168 bool valid = true;
169
170 Slice holderPubKey;
171 if (hasHolderKeyInTx)
172 {
173 holderPubKey = ctx.tx[sfHolderEncryptionKey];
174
175 auto const contextHash =
176 getConvertContextHash(account, issuanceID, ctx.tx.getSeqProxy().value());
177
178 if (auto const ter = verifySchnorrProof(holderPubKey, ctx.tx[sfZKProof], contextHash);
179 !isTesSuccess(ter))
180 {
181 valid = false;
182 }
183 }
184 else
185 {
186 holderPubKey = (*sleMptoken)[sfHolderEncryptionKey];
187 }
188
190 if (hasAuditor)
191 {
192 auditor.emplace(
194 .publicKey = (*sleIssuance)[sfAuditorEncryptionKey],
195 .encryptedAmount = ctx.tx[sfAuditorEncryptedAmount],
196 });
197 }
198
199 auto const blindingFactor = ctx.tx[sfBlindingFactor];
200 if (auto const ter = verifyRevealedAmount(
201 amount,
202 Slice(blindingFactor.data(), blindingFactor.size()),
203 {
204 .publicKey = holderPubKey,
205 .encryptedAmount = ctx.tx[sfHolderEncryptedAmount],
206 },
207 {
208 .publicKey = (*sleIssuance)[sfIssuerEncryptionKey],
209 .encryptedAmount = ctx.tx[sfIssuerEncryptedAmount],
210 },
211 auditor);
212 !isTesSuccess(ter))
213 {
214 valid = false;
215 }
216
217 if (!valid)
218 return tecBAD_PROOF;
219
220 return tesSUCCESS;
221}
222
223TER
225{
226 auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
227
228 auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
229 if (!sleMptoken)
230 {
231 // LCOV_EXCL_START
232 UNREACHABLE(
233 "xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the MPToken "
234 "exists");
235 return tecINTERNAL;
236 // LCOV_EXCL_STOP
237 }
238
239 auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
240 if (!sleIssuance)
241 {
242 // LCOV_EXCL_START
243 UNREACHABLE(
244 "xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the issuance "
245 "exists");
246 return tecINTERNAL;
247 // LCOV_EXCL_STOP
248 }
249
250 auto const amtToConvert = ctx_.tx[sfMPTAmount];
251 auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);
252
253 if (ctx_.tx.isFieldPresent(sfHolderEncryptionKey))
254 (*sleMptoken)[sfHolderEncryptionKey] = ctx_.tx[sfHolderEncryptionKey];
255
256 // Converting decreases regular balance and increases confidential outstanding.
257 // The confidential outstanding tracks total tokens in confidential form globally.
258 auto const currentCOA = (*sleIssuance)[~sfConfidentialOutstandingAmount].valueOr(0);
259 if (amtToConvert > kMaxMpTokenAmount - currentCOA)
260 return tecINTERNAL; // LCOV_EXCL_LINE
261
262 (*sleMptoken)[sfMPTAmount] = amt - amtToConvert;
263 (*sleIssuance)[sfConfidentialOutstandingAmount] = currentCOA + amtToConvert;
264
265 auto const holderEc = ctx_.tx[sfHolderEncryptedAmount];
266 auto const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
267 auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
268
269 // Two cases for Convert:
270 // 1. Holder already has confidential balances -> homomorphically add to inbox
271 // 2. First-time convert -> initialize all confidential balance fields
272 if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
273 sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
274 sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
275 {
276 // Case 1: Add to existing inbox balance (holder will merge later)
277 {
278 auto sum = homomorphicAdd(holderEc, (*sleMptoken)[sfConfidentialBalanceInbox]);
279 if (!sum)
280 {
281 // LCOV_EXCL_START
282 JLOG(ctx_.journal.error())
283 << "ConfidentialMPTConvert failed homomorphic add for holder inbox.";
284 return tecINTERNAL;
285 // LCOV_EXCL_STOP
286 }
287
288 (*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*sum);
289 }
290
291 // homomorphically add issuer's encrypted balance
292 {
293 auto sum = homomorphicAdd(issuerEc, (*sleMptoken)[sfIssuerEncryptedBalance]);
294 if (!sum)
295 {
296 // LCOV_EXCL_START
297 JLOG(ctx_.journal.error())
298 << "ConfidentialMPTConvert failed homomorphic add for issuer balance.";
299 return tecINTERNAL;
300 // LCOV_EXCL_STOP
301 }
302
303 (*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*sum);
304 }
305
306 // homomorphically add auditor's encrypted balance
307 if (auditorEc)
308 {
309 if (!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
310 {
311 // LCOV_EXCL_START
312 UNREACHABLE(
313 "xrpl::ConfidentialMPTConvert::doApply : issuance-level auditing implies "
314 "the MPToken already carries an auditor balance");
315 return tecINTERNAL;
316 // LCOV_EXCL_STOP
317 }
318
319 auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]);
320 if (!sum)
321 {
322 // LCOV_EXCL_START
323 JLOG(ctx_.journal.error())
324 << "ConfidentialMPTConvert failed homomorphic add for auditor balance.";
325 return tecINTERNAL;
326 // LCOV_EXCL_STOP
327 }
328
329 (*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*sum);
330 }
331 }
332 else if (
333 !sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
334 !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
335 !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) &&
336 !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
337 {
338 // Case 2: First-time convert - initialize all confidential fields
339 (*sleMptoken)[sfConfidentialBalanceInbox] = holderEc;
340 (*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc;
341 (*sleMptoken)[sfConfidentialBalanceVersion] = 0;
342
343 if (auditorEc)
344 (*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc;
345
346 // Initialize key epochs when registering the keys.
347 if (view().rules().enabled(featureConfidentialMPTKeyRotation))
348 setMirrorEpochs(*sleIssuance, *sleMptoken);
349
350 // Spending balance starts at zero. Must use canonical zero encryption
351 // (deterministic ciphertext) so the ledger state is reproducible.
352 auto zeroBalance = encryptCanonicalZeroAmount(
353 (*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
354
355 if (!zeroBalance)
356 {
357 // LCOV_EXCL_START
358 UNREACHABLE(
359 "xrpl::ConfidentialMPTConvert::doApply : canonical zero encryption cannot fail "
360 "for an already-valid holder public key");
361 return tecINTERNAL;
362 // LCOV_EXCL_STOP
363 }
364
365 (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance);
366 }
367 else
368 {
369 // both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
370 // exist together
371 // LCOV_EXCL_START
372 UNREACHABLE(
373 "xrpl::ConfidentialMPTConvert::doApply : confidential balance fields must be all "
374 "present or all absent");
375 return tecINTERNAL;
376 // LCOV_EXCL_STOP
377 }
378
379 view().update(sleIssuance);
380 view().update(sleMptoken);
381 return tesSUCCESS;
382}
383
384void
391
392bool
394 STTx const&,
395 TER,
396 XRPAmount,
397 ReadView const&,
398 beast::Journal const&)
399{
400 return true;
401}
402
403} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
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 NotTEC preflight(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 XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
std::int64_t value_type
Definition MPTAmount.h:25
AccountID const & getIssuer() const
Definition MPTIssue.cpp:29
A view into a ledger.
Definition ReadView.h:41
virtual Rules const & rules() const =0
Returns the tx processing rules.
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:182
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:464
SeqProxy getSeqProxy() const
Definition STTx.cpp:198
constexpr std::uint32_t value() const
Definition SeqProxy.h:80
An immutable linear range of bytes.
Definition Slice.h:28
ApplyView & view()
Definition Transactor.h:184
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
AccountID const accountID_
Definition Transactor.h:166
ApplyContext & ctx_
Definition Transactor.h:162
T emplace(T... args)
TER valid(STTx const &tx, ReadView const &view, AccountID const &src, beast::Journal j)
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:567
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool areMirrorsCurrent(SLE const &issuance, SLE const &mptoken)
Checks whether each mirror a holder is required to have is encrypted under the issuance's currently r...
NotTEC checkEncryptedAmountFormat(STObject const &object)
Validates the format of encrypted amount fields in a transaction.
void setMirrorEpochs(SLE const &issuance, SLE &mptoken)
Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
static auto sum(TCollection const &col)
std::optional< Buffer > encryptCanonicalZeroAmount(Slice const &pubKeySlice, AccountID const &account, MPTID const &mptId)
Generates the canonical zero encryption for a specific MPToken.
UInt256 getConvertContextHash(AccountID const &account, UInt192 const &issuanceID, std::uint32_t sequence)
Generates the context hash for ConfidentialMPTConvert transactions.
constexpr std::uint32_t kConfidentialFeeMultiplier
Extra base fee multiplier charged to confidential MPT transactions.
Definition Protocol.h:551
TER checkFrozen(ReadView const &view, AccountID const &account, Issue const &issue)
@ tefINTERNAL
Definition TER.h:168
bool isValidCompressedECPoint(Slice const &buffer)
Verifies that a buffer contains a valid, parsable compressed EC point.
constexpr std::size_t kEcSchnorrProofLength
Length of Schnorr ZKProof for public key registration (compact form) in bytes.
Definition Protocol.h:500
TER verifyRevealedAmount(uint64_t const amount, Slice const &blindingFactor, ConfidentialRecipient const &holder, ConfidentialRecipient const &issuer, std::optional< ConfidentialRecipient > const &auditor)
Verifies revealed amount encryptions for all recipients.
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:614
@ temMALFORMED
Definition TER.h:75
@ temBAD_AMOUNT
Definition TER.h:77
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
TERSubset< CanCvtToTER > TER
Definition TER.h:654
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.
@ tecOBJECT_NOT_FOUND
Definition TER.h:334
@ tecINTERNAL
Definition TER.h:318
@ tecINSUFFICIENT_FUNDS
Definition TER.h:333
@ tecBAD_PROOF
Definition TER.h:376
@ tecNO_PERMISSION
Definition TER.h:313
@ tecDUPLICATE
Definition TER.h:323
TER verifySchnorrProof(Slice const &pubKeySlice, Slice const &proofSlice, UInt256 const &contextHash)
Verifies a Schnorr proof of knowledge of an ElGamal private key.
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:297
std::optional< Buffer > homomorphicAdd(Slice const &a, Slice const &b)
Homomorphically adds two ElGamal ciphertexts.
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j, SpendableHandling includeFullBalance=SpendableHandling::SimpleBalance)
@ tesSUCCESS
Definition TER.h:250
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:92
ReadView const & view
Definition Transactor.h:95
beast::Journal const j
Definition Transactor.h:100
State information when preflighting a tx.
Definition Transactor.h:39