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