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/core/ServiceRegistry.h>
7#include <xrpl/ledger/ReadView.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/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 if (!ctx.rules.enabled(featureConfidentialTransfer))
30 return temDISABLED;
31
32 // issuer cannot convert
33 if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
34 return temMALFORMED;
35
36 if (ctx.tx[sfMPTAmount] > kMaxMpTokenAmount)
37 return temBAD_AMOUNT;
38
39 if (ctx.tx.isFieldPresent(sfHolderEncryptionKey))
40 {
41 if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
42 return temMALFORMED;
43
44 // proof of knowledge of the secret key corresponding to the provided
45 // public key is needed when holder ec public key is being set.
46 if (!ctx.tx.isFieldPresent(sfZKProof))
47 return temMALFORMED;
48
49 // verify schnorr proof length when registering holder ec public key
50 if (ctx.tx[sfZKProof].size() != kEcSchnorrProofLength)
51 return temMALFORMED;
52 }
53 else
54 {
55 // Either both sfHolderEncryptionKey and sfZKProof should be present, or both should be
56 // absent.
57 if (ctx.tx.isFieldPresent(sfZKProof))
58 return temMALFORMED;
59 }
60
61 // check encrypted amount format after the above basic checks
62 // this check is more expensive so put it at the end
63 if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
64 return res;
65
66 return tesSUCCESS;
67}
68
74
75TER
77{
78 auto const account = ctx.tx[sfAccount];
79 auto const issuanceID = ctx.tx[sfMPTokenIssuanceID];
80 auto const amount = ctx.tx[sfMPTAmount];
81
82 // ensure that issuance exists
83 auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(issuanceID));
84 if (!sleIssuance)
86
87 if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) ||
88 !sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
89 {
90 return tecNO_PERMISSION;
91 }
92
93 // already checked in preflight, but should also check that issuer on the
94 // issuance isn't the account either
95 if (sleIssuance->getAccountID(sfIssuer) == account)
96 return tefINTERNAL; // LCOV_EXCL_LINE
97
98 bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
99 bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
100
101 // tx must include auditor ciphertext if the issuance has enabled
102 // auditing, and must not include it if auditing is not enabled
103 if (requiresAuditor != hasAuditor)
104 return tecNO_PERMISSION;
105
106 auto const sleMptoken = ctx.view.read(keylet::mptoken(issuanceID, account));
107 if (!sleMptoken)
108 return tecOBJECT_NOT_FOUND;
109
110 auto const mptIssue = MPTIssue{issuanceID};
111
112 // Explicit freeze and auth checks are required because accountHolds
113 // with ZeroIfFrozen/ZeroIfUnauthorized only implicitly rejects
114 // non-zero amounts. A zero-amount convert would bypass those implicit
115 // checks, allowing frozen or unauthorized accounts to register ElGamal
116 // keys and initialize confidential balance fields.
117
118 // Check lock
119 if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
120 return ter;
121
122 // Check auth
123 if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
124 return ter;
125
126 auto const mptAmount =
127 STAmount(MPTAmount{static_cast<MPTAmount::value_type>(amount)}, mptIssue);
128 if (accountHolds(
129 ctx.view,
130 account,
131 mptIssue,
134 ctx.j) < mptAmount)
135 {
137 }
138
139 auto const hasHolderKeyOnLedger = sleMptoken->isFieldPresent(sfHolderEncryptionKey);
140 auto const hasHolderKeyInTx = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
141
142 // must have pk to convert
143 if (!hasHolderKeyOnLedger && !hasHolderKeyInTx)
144 return tecNO_PERMISSION;
145
146 // can't update if there's already a pk
147 if (hasHolderKeyOnLedger && hasHolderKeyInTx)
148 return tecDUPLICATE;
149
150 // Run all verifications before returning any error to prevent timing attacks
151 // that could reveal which proof failed.
152 bool valid = true;
153
154 Slice holderPubKey;
155 if (hasHolderKeyInTx)
156 {
157 holderPubKey = ctx.tx[sfHolderEncryptionKey];
158
159 auto const contextHash =
160 getConvertContextHash(account, issuanceID, ctx.tx.getSeqProxy().value());
161
162 if (auto const ter = verifySchnorrProof(holderPubKey, ctx.tx[sfZKProof], contextHash);
163 !isTesSuccess(ter))
164 {
165 valid = false;
166 }
167 }
168 else
169 {
170 holderPubKey = (*sleMptoken)[sfHolderEncryptionKey];
171 }
172
174 if (hasAuditor)
175 {
176 auditor.emplace(
178 .publicKey = (*sleIssuance)[sfAuditorEncryptionKey],
179 .encryptedAmount = ctx.tx[sfAuditorEncryptedAmount],
180 });
181 }
182
183 auto const blindingFactor = ctx.tx[sfBlindingFactor];
184 if (auto const ter = verifyRevealedAmount(
185 amount,
186 Slice(blindingFactor.data(), blindingFactor.size()),
187 {
188 .publicKey = holderPubKey,
189 .encryptedAmount = ctx.tx[sfHolderEncryptedAmount],
190 },
191 {
192 .publicKey = (*sleIssuance)[sfIssuerEncryptionKey],
193 .encryptedAmount = ctx.tx[sfIssuerEncryptedAmount],
194 },
195 auditor);
196 !isTesSuccess(ter))
197 {
198 valid = false;
199 }
200
201 if (!valid)
202 return tecBAD_PROOF;
203
204 return tesSUCCESS;
205}
206
207TER
209{
210 auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
211
212 auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
213 if (!sleMptoken)
214 return tecINTERNAL; // LCOV_EXCL_LINE
215
216 auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
217 if (!sleIssuance)
218 return tecINTERNAL; // LCOV_EXCL_LINE
219
220 auto const amtToConvert = ctx_.tx[sfMPTAmount];
221 auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);
222
223 if (ctx_.tx.isFieldPresent(sfHolderEncryptionKey))
224 (*sleMptoken)[sfHolderEncryptionKey] = ctx_.tx[sfHolderEncryptionKey];
225
226 // Converting decreases regular balance and increases confidential outstanding.
227 // The confidential outstanding tracks total tokens in confidential form globally.
228 auto const currentCOA = (*sleIssuance)[~sfConfidentialOutstandingAmount].valueOr(0);
229 if (amtToConvert > kMaxMpTokenAmount - currentCOA)
230 return tecINTERNAL; // LCOV_EXCL_LINE
231
232 (*sleMptoken)[sfMPTAmount] = amt - amtToConvert;
233 (*sleIssuance)[sfConfidentialOutstandingAmount] = currentCOA + amtToConvert;
234
235 auto const holderEc = ctx_.tx[sfHolderEncryptedAmount];
236 auto const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
237 auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
238
239 // Two cases for Convert:
240 // 1. Holder already has confidential balances -> homomorphically add to inbox
241 // 2. First-time convert -> initialize all confidential balance fields
242 if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
243 sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
244 sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
245 {
246 // Case 1: Add to existing inbox balance (holder will merge later)
247 {
248 auto sum = homomorphicAdd(holderEc, (*sleMptoken)[sfConfidentialBalanceInbox]);
249 if (!sum)
250 {
251 // LCOV_EXCL_START
252 JLOG(ctx_.journal.error())
253 << "ConfidentialMPTConvert failed homomorphic add for holder inbox.";
254 return tecINTERNAL;
255 // LCOV_EXCL_STOP
256 }
257
258 (*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*sum);
259 }
260
261 // homomorphically add issuer's encrypted balance
262 {
263 auto sum = homomorphicAdd(issuerEc, (*sleMptoken)[sfIssuerEncryptedBalance]);
264 if (!sum)
265 {
266 // LCOV_EXCL_START
267 JLOG(ctx_.journal.error())
268 << "ConfidentialMPTConvert failed homomorphic add for issuer balance.";
269 return tecINTERNAL;
270 // LCOV_EXCL_STOP
271 }
272
273 (*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*sum);
274 }
275
276 // homomorphically add auditor's encrypted balance
277 if (auditorEc)
278 {
279 if (!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
280 return tecINTERNAL; // LCOV_EXCL_LINE
281
282 auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]);
283 if (!sum)
284 {
285 // LCOV_EXCL_START
286 JLOG(ctx_.journal.error())
287 << "ConfidentialMPTConvert failed homomorphic add for auditor balance.";
288 return tecINTERNAL;
289 // LCOV_EXCL_STOP
290 }
291
292 (*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*sum);
293 }
294 }
295 else if (
296 !sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
297 !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
298 !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) &&
299 !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
300 {
301 // Case 2: First-time convert - initialize all confidential fields
302 (*sleMptoken)[sfConfidentialBalanceInbox] = holderEc;
303 (*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc;
304 (*sleMptoken)[sfConfidentialBalanceVersion] = 0;
305
306 if (auditorEc)
307 (*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc;
308
309 // Spending balance starts at zero. Must use canonical zero encryption
310 // (deterministic ciphertext) so the ledger state is reproducible.
311 auto zeroBalance = encryptCanonicalZeroAmount(
312 (*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
313
314 if (!zeroBalance)
315 return tecINTERNAL; // LCOV_EXCL_LINE
316
317 (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance);
318 }
319 else
320 {
321 // both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
322 // exist together
323 return tecINTERNAL; // LCOV_EXCL_LINE
324 }
325
326 view().update(sleIssuance);
327 view().update(sleMptoken);
328 return tesSUCCESS;
329}
330
331void
338
339bool
341 STTx const&,
342 TER,
343 XRPAmount,
344 ReadView const&,
345 beast::Journal const&)
346{
347 return true;
348}
349
350} // 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 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:22
AccountID const & getIssuer() const
Definition MPTIssue.cpp:29
A view into a ledger.
Definition ReadView.h:31
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)
TER valid(STTx const &tx, ReadView const &view, AccountID const &src, beast::Journal j)
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
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)
Definition BookStep.cpp:993
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: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 kEcSchnorrProofLength
Length of Schnorr ZKProof for public key registration (compact form) in bytes.
Definition Protocol.h:336
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:594
uint256 getConvertContextHash(AccountID const &account, uint192 const &issuanceID, std::uint32_t sequence)
Generates the context hash for ConfidentialMPTConvert transactions.
@ temMALFORMED
Definition TER.h:73
@ temDISABLED
Definition TER.h:100
@ temBAD_AMOUNT
Definition TER.h:75
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.
@ tecOBJECT_NOT_FOUND
Definition TER.h:324
@ tecINTERNAL
Definition TER.h:308
@ tecINSUFFICIENT_FUNDS
Definition TER.h:323
@ tecBAD_PROOF
Definition TER.h:366
@ tecNO_PERMISSION
Definition TER.h:303
@ tecDUPLICATE
Definition TER.h:313
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:238
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:240
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