Compare commits

...

40 Commits

Author SHA1 Message Date
Shawn Xie
6c38086f17 ConfidentialConvert with Equality Proof (#6177) 2026-01-07 16:17:07 -05:00
Shawn Xie
3e9dc276ed add back clawback hash (#6175) 2026-01-06 12:21:00 -05:00
Shawn Xie
abf7a62b1f Refactor proof (#6168) 2026-01-05 12:00:41 -05:00
yinyiqian1
bd3a6e1631 Support equality proof for confidential clawback (#6149) 2026-01-02 11:48:06 -05:00
yinyiqian1
7c0bd419a4 support mutability for MPTPrivacy (#6137)
Update lsfMPTNoConfidentialTransfer to lsfMPTPrivacy
Add flag lsmfMPTPrivacy to control the mutability of lsfMPTPrivacy.
disallow mutating lsfMPTPrivacy when lsfMPTPrivacy is not set.
disallow mutating lsfMPTPrivacy when there's confidential outstanding amount.
2025-12-10 17:10:33 -05:00
yinyiqian1
d3126959e7 Merge pull request #6123 from yinyiqian1/merge
Merge remote-tracking branch 'origin/develop' into 'ripple/confidential-transfer'
2025-12-09 10:49:11 -05:00
yinyiqian1
67e8e89e0f copyright fix 2025-12-08 18:36:34 -05:00
yinyiqian1
4e4326a174 trigger ci 2025-12-08 18:25:47 -05:00
yinyiqian1
5397bd6d6e fix naming 2025-12-08 17:58:32 -05:00
yinyiqian1
6dece25cc3 fix test failure 2025-12-08 17:34:20 -05:00
yinyiqian1
d9da8733be resolve pre-commit clang-format 2025-12-08 16:45:41 -05:00
yinyiqian1
f6f51451e7 Merge remote-tracking branch 'origin/develop' into merge 2025-12-08 15:09:04 -05:00
Shawn Xie
b94c95b3e9 Change err code (#6050) 2025-11-18 14:19:41 -05:00
yinyiqian1
8365148b5c feat: support ConfidentialClawback and add tests (#6023) 2025-11-13 14:24:40 -05:00
Shawn Xie
c03866bf0f Variable rename (#6028) 2025-11-12 11:58:05 -05:00
Shawn Xie
389afc5f06 Add deposit preauth and other checks (#6011) 2025-11-10 10:52:23 -05:00
Shawn Xie
7b04eaae81 ConvertBack preclaim tests (#6006) 2025-11-05 13:58:52 -05:00
Shawn Xie
1343019509 ConvertBack tests (#6005) 2025-11-05 13:52:55 -05:00
Shawn Xie
cd75e630a2 Change ConfidentialSend preflight error code (#5994) 2025-11-03 18:46:27 -05:00
Shawn Xie
ec57fbdc5f Merge remote-tracking branch 'upstream/develop' into confidential-transfer 2025-11-03 18:42:41 -05:00
Shawn Xie
4fe67f5715 ConvertBack preflight tests (#5991) 2025-11-03 15:58:32 -05:00
Shawn Xie
44d885e39b Basic ConvertBack test (#5979) 2025-10-31 11:46:24 -04:00
yinyiqian1
3af758145c Check auth for ConfidentialSend (#5968) 2025-10-30 11:02:46 -04:00
yinyiqian1
f3d4d4341b add ciphertext check for ConfidentialSend (#5964) 2025-10-29 12:10:48 -04:00
Shawn Xie
ddb518ad09 MergeInbox tests (#5949) 2025-10-28 13:21:11 -04:00
Shawn Xie
3899e3f36c Add auth checks for convert (#5937) 2025-10-24 11:42:43 -04:00
yinyiqian1
e4a8ba51f9 check lock in ConfidentialSend (#5933) 2025-10-23 12:58:38 -04:00
Shawn Xie
35e4fad557 Add ciphertext check (#5930) 2025-10-23 11:57:18 -04:00
yinyiqian1
8e9cb3c1da support ConfidentialSend (#5921) 2025-10-22 12:02:00 -04:00
Shawn Xie
18d92058e3 MergeInbox (#5922) 2025-10-22 11:30:44 -04:00
Shawn Xie
f24d584f29 ConfidentialConvert tests (#5911) 2025-10-20 14:39:16 -04:00
Shawn Xie
da3fbcd25b Remove unused header file (#5908) 2025-10-17 16:42:08 -04:00
Shawn Xie
daa1303b5a Update decryption test helper function (#5907) 2025-10-17 14:19:19 -04:00
Shawn Xie
a636fe5871 Update test framework for encryption (#5906) 2025-10-17 14:04:54 -04:00
Shawn Xie
bbc3071fd1 Update mpt-crypto with zero encryption (#5905) 2025-10-17 11:41:39 -04:00
Shawn Xie
8fdc639206 ConfidentialConvert (#5901)
ConfidentialConvert and some test framework update
2025-10-16 14:31:14 -04:00
Shawn Xie
5a89641d98 remove duplicate code 2025-10-07 15:52:18 -04:00
Shawn Xie
beefa248a6 Merge remote-tracking branch 'upstream/develop' into confidential-transfer 2025-10-07 15:00:14 -04:00
Shawn Xie
e919a25ecb Merge develop into ripple/confidential-transfer (#5835)
* Fix: Don't flag consensus as stalled prematurely (#5658)

Fix stalled consensus detection to prevent false positives in situations where there are no disputed transactions.

Stalled consensus detection was added to 2.5.0 in response to a network consensus halt that caused a round to run for over an hour. However, it has a flaw that makes it very easy to have false positives. Those false positives are usually mitigated by other checks that prevent them from having an effect, but there have been several instances of validators "running ahead" because there are circumstances where the other checks are "successful", allowing the stall state to be checked.

* Set version to 2.5.1

* fix: Skip processing transaction batch if the batch is empty (#5670)

Avoids an assertion failure in NetworkOPsImp::apply in the unlikely event that all incoming transactions are invalid.

* Fix: EscrowTokenV1 (#5571)

* resolves an accounting inconsistency in MPT escrows where transfer fees were not properly handled when unlocking escrowed tokens.

* refactor: Wrap GitHub CI conditionals in curly braces (#5796)

This change wraps all GitHub conditionals in `${{ .. }}`, both for consistency and to reduce unexpected failures, because it was previously noticed that not all conditionals work without those curly braces.

* Only notify clio for PRs targeting the release and master branches (#5794)

Clio should only be notified when releases are about to be made, instead of for all PR, so this change only notifies Clio when a PR targets the release or master branch.

* Support DynamicMPT XLS-94d (#5705)

* extends the functionality of the MPTokenIssuanceSet transaction, allowing the issuer to update fields or flags that were explicitly marked as mutable during creation.

* Bugfix: Adds graceful peer disconnection (#5669)

The XRPL establishes connections in three stages: first a TCP connection, then a TLS/SSL handshake to secure the connection, and finally an upgrade to the bespoke XRP Ledger peer-to-peer protocol. During connection termination, xrpld directly closes the TCP connection, bypassing the TLS/SSL shutdown handshake. This makes peer disconnection diagnostics more difficult - abrupt TCP termination appears as if the peer crashed rather than disconnected gracefully.

This change refactors the connection lifecycle with the following changes:
- Enhanced outgoing connection logic with granular timeouts for each connection stage (TCP, TLS, XRPL handshake) to improve diagnostic capabilities
- Updated both PeerImp and ConnectAttempt to use proper asynchronous TLS shutdown procedures for graceful connection termination

* Downgrade to boost 1.83

* Set version to 2.6.1-rc1

* chore: Use self hosted windows runners (#5780)

This changes switches from the GitHub-managed Windows runners to self-hosted runners to significantly reduce build time.

* Rename mutable flags (#5797)

This is a minor change on top of #5705

* fix(amendment): Add missing fields for keylets to ledger objects (#5646)

This change adds a fix amendment (`fixIncludeKeyletFields`) that adds:
* `sfSequence` to `Escrow` and `PayChannel`
* `sfOwner` to `SignerList`
* `sfOracleDocumentID` to `Oracle`

This ensures that all ledger entries hold all the information needed to determine their keylet.

* chore: Limits CI build and test parallelism to reduce resource contention (#5799)

GitHub runners have a limit on how many concurrent jobs they can actually process (even though they will try to run them all at the same time), and similarly the Conan remote cannot handle hundreds of concurrent requests. Previously, the Conan dependency uploading was already limited to max 10 jobs running in parallel, and this change makes the same change to the build+test workflow.

* chore: Build and test all configs for daily scheduled run (#5801)

This change re-enables building and testing all configurations, but only for the daily scheduled run. Previously all configurations were run for each merge into the develop branch, but that overwhelmed both the GitHub runners and the Conan remote, and thus they were limited to just a subset of configurations. Now that the number of jobs is limited via `max-parallel: 10`, we should be able to safely enable building all configurations again. However, building them all once a day instead of for each PR merge should be sufficient.

* chore: Add unit tests dir to code coverage excludes (#5803)

This change excludes unit test code from code coverage reporting.

* refactor: Modularise ledger (#5493)

This change moves the ledger code to libxrpl.

* Mark PermissionDelegation as unsupported

* Set version to 2.6.1-rc2

* Miscellaneous refactors and updates (#5590)

- Added a new Invariant: `ValidPseudoAccounts` which checks that all pseudo-accounts behave consistently through creation and updates, and that no "real" accounts look like pseudo-accounts (which means they don't have a 0 sequence). 
- `to_short_string(base_uint)`. Like `to_string`, but only returns the first 8 characters. (Similar to how a git commit ID can be abbreviated.) Used as a wrapped sink to prefix most transaction-related messages. More can be added later.
- `XRPL_ASSERT_PARTS`. Convenience wrapper for `XRPL_ASSERT`, which takes the `function` and `description` as separate parameters.
- `SField::sMD_PseudoAccount`. Metadata option for `SField` definitions to indicate that the field, if set in an `AccountRoot` indicates that account is a pseudo-account. Removes the need for hard-coded field lists all over the place. Added the flag to `AMMID` and `VaultID`.
- Added functionality to `SField` ctor to detect both code and name collisions using asserts. And require all SFields to have a name
- Convenience type aliases `STLedgerEntry::const_pointer` and `STLedgerEntry::const_ref`. (`SLE` is an alias to `STLedgerEntry`.)
- Generalized `feeunit.h` (`TaggedFee`) into `unit.h` (`ValueUnit`) and added new "BIPS"-related tags for future use. Also refactored the type restrictions to use Concepts.
- Restructured `transactions.macro` to do two big things
	1. Include the `#include` directives for transactor header files directly in the macro file. Removes the need to update `applySteps.cpp` and the resulting conflicts.
	2. Added a `privileges` parameter to the `TRANSACTION` macro, which specifies some of the operations a transaction is allowed to do. These `privileges` are enforced by invariant checks. Again, removed the need to update scattered lists of transaction types in various checks.
- Unit tests:
	1.  Moved more helper functions into `TestHelpers.h` and `.cpp`. 
	2. Cleaned up the namespaces to prevent / mitigate random collisions and ambiguous symbols, particularly in unity builds.
	3. Generalized `Env::balance` to add support for `MPTIssue` and `Asset`.
	4. Added a set of helper classes to simplify `Env` transaction parameter classes: `JTxField`, `JTxFieldWrapper`, and a bunch of classes derived or aliased from it. For an example of how awesome it is, check the changes `src/test/jtx/escrow.h` for how much simpler the definitions are for `finish_time`, `cancel_time`, `condition`, and `fulfillment`. 
	5. Generalized several of the amount-related helper classes to understand `Asset`s.
     6. `env.balance` for an MPT issuer will return a negative number (or 0) for consistency with IOUs.

* refactor: Simplify STParsedJSON with some helper functions (#5591)

- Add code coverage for STParsedJSON edge cases

Co-authored-by: Denis Angell <dangell@transia.co>

* test: Add STInteger and STParsedJSON tests (#5726)

This change is to improve code coverage (and to simplify #5720 and #5725); there is otherwise no change in functionality. The change adds basic tests for `STInteger` and `STParsedJSON`, so it becomes easier to test smaller changes to the types, as well as removes `STParsedJSONArray`, since it is not used anywhere (including in Clio).

* Revert "Update Conan dependencies: OpenSSL" (#5807)

This change reverts #5617, because it will require extensive testing that will take up more time than we have before the next scheduled release.

Reverting this change does not mean we are abandoning it. We aim to pick it back up once there's a sufficient time window to allow for testing on multiple distros running a mixture of OpenSSL 1.x and 3.x.

* docs: Add warning about using std::counting_semaphore (#5595)

This adds a comment to avoid using `std::counting_semaphore` until the minimum compiler versions of GCC and Clang have been updated to no longer contain the bug that is present in older compilers.

* Improve ValidatorList invalid UNL manifest logging (#5804)

This change raises logging severity from `INFO` to `WARN` when handling UNL manifest signed with an unexpected / invalid key. It also changes the internal error code for an invalid format of UNL manifest to `invalid` (from `untrusted`).

This is a follow up to problems experienced by an UNL node due to old manifest key configured in `validators.txt`, which would be easier to diagnose with improved logging.

It also replaces a log line with `UNREACHABLE` for an impossible situation when we match UNL manifest key against a configured key which has an invalid type (we cannot configure such a key because of checks when loading configured keys).

* chore: Pin all CI Docker tags (#5813)

To avoid surprises and ensure reproducibility, this change pins all CI Docker image tags to the latest version in the XRPLF/CI repo.

* change `fixPriceOracleOrder` to `Supported::yes` (#5749)

* fix: Address http header case sensitivity (#5767)

This change makes the regex in `HttpClient.cpp` that matches the content-length http header case insensitive to improve compatibility, as http headers are case insensitive.

* test: add more comprehensive tests for `FeeVote` (#5746)

This change adds more comprehensive tests for the `FeeVote` module, which previously only checked the basics, and not the more comprehensive flows in that class.

* ci: Call all reusable workflows reusable (#5818)

* Add `STInt32` as a new `SType` (#5788)

This change adds `STInt32` as a new `SType` under the `STInteger` umbrella, with `SType` value `12`. This is the first and only `STInteger` type that supports negative values.

* switch `fixIncludeKeyletFields` to `Supported::yes` (#5819)

* refactor: Restructure Transactor::preflight to reduce boilerplate (#5592)

* Restructures `Transactor::preflight` to create several functions that will remove the need for error-prone boilerplate code in derived classes' implementations of `preflight`.

* refactor: Add support for extra transaction signatures (#5594)

* Restructures Transactor signature checking code to be able to handle a `sigObject`, which may be the full transaction, or may be an object field containing a separate signature. Either way, the `sigObject` can be a single- or multi-sign signature.

* ci: Upload artifacts during build and test in a separate job (#5817)

* chore: Set free-form CI inputs as env vars (#5822)

This change moves CI values that could be user-provided into environment variables.

* Rename flags for DynamicMPT (#5820)

* Set version to 2.6.1

* fix: FD/handle guarding + exponential backoff (#5823)

* fix: Transaction sig checking functions do not get a full context (#5829)

Fixes a (currently harmless) bug introduced by PR #5594

* Remove bogus coverage warning (#5838)

* fix return type

---------

Co-authored-by: Ed Hennis <ed@ripple.com>
Co-authored-by: Jingchen <a1q123456@users.noreply.github.com>
Co-authored-by: Denis Angell <dangell@transia.co>
Co-authored-by: Bart <bthomee@users.noreply.github.com>
Co-authored-by: yinyiqian1 <yqian@ripple.com>
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Co-authored-by: Bronek Kozicki <brok@incorrekt.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
Co-authored-by: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com>
Co-authored-by: tequ <git@tequ.dev>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2025-10-07 14:14:34 -04:00
Shawn Xie
c3fdbc0430 SFields and formats (#5795) 2025-10-01 17:02:11 +00:00
29 changed files with 6446 additions and 77 deletions

View File

@@ -0,0 +1,282 @@
#ifndef XRPL_PROTOCOL_CONFIDENTIALTRANSFER_H_INCLUDED
#define XRPL_PROTOCOL_CONFIDENTIALTRANSFER_H_INCLUDED
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/detail/secp256k1.h>
#include <secp256k1.h>
namespace ripple {
void
addCommonZKPFields(
Serializer& s,
std::uint16_t txType,
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount);
uint256
getClawbackContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount,
AccountID const& holder);
uint256
getConvertContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount);
/**
* @brief Generates a new secp256k1 key pair.
*/
SECP256K1_API int
secp256k1_elgamal_generate_keypair(
secp256k1_context const* ctx,
unsigned char* privkey,
secp256k1_pubkey* pubkey);
/**
* @brief Encrypts a 64-bit amount using ElGamal.
*/
SECP256K1_API int
secp256k1_elgamal_encrypt(
secp256k1_context const* ctx,
secp256k1_pubkey* c1,
secp256k1_pubkey* c2,
secp256k1_pubkey const* pubkey_Q,
uint64_t amount,
unsigned char const* blinding_factor);
/**
* @brief Decrypts an ElGamal ciphertext to recover the amount.
*/
SECP256K1_API int
secp256k1_elgamal_decrypt(
secp256k1_context const* ctx,
uint64_t* amount,
secp256k1_pubkey const* c1,
secp256k1_pubkey const* c2,
unsigned char const* privkey);
/**
* @brief Homomorphically adds two ElGamal ciphertexts.
*/
SECP256K1_API int
secp256k1_elgamal_add(
secp256k1_context const* ctx,
secp256k1_pubkey* sum_c1,
secp256k1_pubkey* sum_c2,
secp256k1_pubkey const* a_c1,
secp256k1_pubkey const* a_c2,
secp256k1_pubkey const* b_c1,
secp256k1_pubkey const* b_c2);
/**
* @brief Homomorphically subtracts two ElGamal ciphertexts.
*/
SECP256K1_API int
secp256k1_elgamal_subtract(
secp256k1_context const* ctx,
secp256k1_pubkey* diff_c1,
secp256k1_pubkey* diff_c2,
secp256k1_pubkey const* a_c1,
secp256k1_pubkey const* a_c2,
secp256k1_pubkey const* b_c1,
secp256k1_pubkey const* b_c2);
/**
* @brief Generates the canonical encrypted zero for a given MPT token instance.
*
* This ciphertext represents a zero balance for a specific account's holding
* of a token defined by its MPTokenIssuanceID.
*
* @param[in] ctx A pointer to a valid secp256k1 context.
* @param[out] enc_zero_c1 The C1 component of the canonical ciphertext.
* @param[out] enc_zero_c2 The C2 component of the canonical ciphertext.
* @param[in] pubkey The ElGamal public key of the account holder.
* @param[in] account_id A pointer to the 20-byte AccountID.
* @param[in] mpt_issuance_id A pointer to the 24-byte MPTokenIssuanceID.
*
* @return 1 on success, 0 on failure.
*/
SECP256K1_API int
generate_canonical_encrypted_zero(
secp256k1_context const* ctx,
secp256k1_pubkey* enc_zero_c1,
secp256k1_pubkey* enc_zero_c2,
secp256k1_pubkey const* pubkey,
unsigned char const* account_id, // 20 bytes
unsigned char const* mpt_issuance_id // 24 bytes
);
/**
* Generates a cryptographically secure 32-byte scalar (private key).
* @return 1 on success, 0 on failure.
*/
SECP256K1_API int
generate_random_scalar(
secp256k1_context const* ctx,
unsigned char* scalar_bytes);
/**
* Computes the point M = amount * G.
* IMPORTANT: This function MUST NOT be called with amount = 0.
*/
SECP256K1_API int
compute_amount_point(
secp256k1_context const* ctx,
secp256k1_pubkey* mG,
uint64_t amount);
/**
* Builds the challenge hash input for the NON-ZERO amount case.
* Output buffer must be 253 bytes.
*/
SECP256K1_API void
build_challenge_hash_input_nonzero(
unsigned char* hash_input,
secp256k1_pubkey const* c1,
secp256k1_pubkey const* c2,
secp256k1_pubkey const* pk,
secp256k1_pubkey const* mG,
secp256k1_pubkey const* T1,
secp256k1_pubkey const* T2,
unsigned char const* tx_context_id);
/**
* Builds the challenge hash input for the ZERO amount case.
* Output buffer must be 220 bytes.
*/
SECP256K1_API void
build_challenge_hash_input_zero(
unsigned char* hash_input,
secp256k1_pubkey const* c1,
secp256k1_pubkey const* c2,
secp256k1_pubkey const* pk,
secp256k1_pubkey const* T1,
secp256k1_pubkey const* T2,
unsigned char const* tx_context_id);
/**
* @brief Proves that a commitment (C1, C2) encrypts a specific plaintext
* 'amount'.
*/
SECP256K1_API int
secp256k1_equality_plaintext_prove(
secp256k1_context const* ctx,
unsigned char* proof,
secp256k1_pubkey const* c1,
secp256k1_pubkey const* c2,
secp256k1_pubkey const* pk_recipient,
uint64_t amount,
unsigned char const* randomness_r,
unsigned char const* tx_context_id);
/**
* @brief Verifies the proof generated by secp256k1_equality_plaintext_prove.
*/
SECP256K1_API int
secp256k1_equality_plaintext_verify(
secp256k1_context const* ctx,
unsigned char const* proof,
secp256k1_pubkey const* c1,
secp256k1_pubkey const* c2,
secp256k1_pubkey const* pk_recipient,
uint64_t amount,
unsigned char const* tx_context_id);
// breaks a 66-byte encrypted amount into two 33-byte components
// then parses each 33-byte component into 64-byte secp256k1_pubkey format
bool
makeEcPair(Slice const& buffer, secp256k1_pubkey& out1, secp256k1_pubkey& out2);
// serialize two secp256k1_pubkey components back into compressed 66-byte form
bool
serializeEcPair(
secp256k1_pubkey const& in1,
secp256k1_pubkey const& in2,
Buffer& buffer);
/**
* @brief Verifies that a buffer contains two valid, parsable EC public keys.
* @param buffer The input buffer containing two concatenated components.
* @return true if both components can be parsed successfully, false otherwise.
*/
bool
isValidCiphertext(Slice const& buffer);
TER
homomorphicAdd(Slice const& a, Slice const& b, Buffer& out);
TER
homomorphicSubtract(Slice const& a, Slice const& b, Buffer& out);
TER
proveEquality(
Slice const& proof,
Slice const& encAmt, // encrypted amount
Slice const& pubkey,
uint64_t const amount,
uint256 const& txHash, // Transaction context data
std::uint32_t const spendVersion);
// returns ciphertext and the blinding factor used
std::pair<Buffer, Buffer>
encryptAmount(uint64_t amt, Slice const& pubKeySlice);
Buffer
encryptCanonicalZeroAmount(
Slice const& pubKeySlice,
AccountID const& account,
MPTID const& mptId);
TER
verifyConfidentialSendProof(
Slice const& proof,
Slice const& encSenderBalance,
Slice const& encSenderAmt,
Slice const& encDestAmt,
Slice const& encIssuerAmt,
Slice const& senderPubKey,
Slice const& destPubKey,
Slice const& issuerPubKey,
std::uint32_t const version,
uint256 const& txHash);
TER
verifyEqualityProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
Slice const& ciphertext,
uint256 const& contextHash);
TER
verifyClawbackEqualityProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
Slice const& ciphertext,
uint256 const& contextHash);
std::vector<Buffer>
getEqualityProofs(Slice const& zkp);
} // namespace ripple
#endif

View File

@@ -168,6 +168,7 @@ enum LedgerSpecificFlags {
lsfMPTCanTrade = 0x00000010,
lsfMPTCanTransfer = 0x00000020,
lsfMPTCanClawback = 0x00000040,
lsfMPTCanPrivacy = 0x00000080,
lsmfMPTCanMutateCanLock = 0x00000002,
lsmfMPTCanMutateRequireAuth = 0x00000004,
@@ -177,6 +178,8 @@ enum LedgerSpecificFlags {
lsmfMPTCanMutateCanClawback = 0x00000040,
lsmfMPTCanMutateMetadata = 0x00010000,
lsmfMPTCanMutateTransferFee = 0x00020000,
// if set, lsfMPTCanPrivacy can not be mutated
lsmfMPTCannotMutatePrivacy = 0x00040000,
// ltMPTOKEN
lsfMPTAuthorized = 0x00000002,

View File

@@ -297,6 +297,20 @@ std::size_t constexpr permissionMaxSize = 10;
/** The maximum number of transactions that can be in a batch. */
std::size_t constexpr maxBatchTxCount = 8;
/** EC ElGamal ciphertext length 33-byte */
std::size_t constexpr ecGamalEncryptedLength = 33;
/** EC ElGamal ciphertext length: two 33-byte components concatenated */
std::size_t constexpr ecGamalEncryptedTotalLength = 66;
/** Length of equality ZKProof */
std::size_t constexpr ecEqualityProofLength = 98;
/** Length of EC public key */
std::size_t constexpr ecPubKeyLength = 64;
/** Length of EC private key */
std::size_t constexpr ecPrivKeyLength = 32;
} // namespace ripple
#endif

View File

@@ -122,6 +122,7 @@ enum TEMcodes : TERUnderlyingType {
temARRAY_TOO_LARGE,
temBAD_TRANSFER_FEE,
temINVALID_INNER_BATCH,
temBAD_CIPHERTEXT,
};
//------------------------------------------------------------------------------
@@ -347,6 +348,7 @@ enum TECcodes : TERUnderlyingType {
// backward compatibility with historical data on non-prod networks, can be
// reclaimed after those networks reset.
tecNO_DELEGATE_PERMISSION = 198,
tecBAD_PROOF = 199
};
//------------------------------------------------------------------------------

View File

@@ -132,8 +132,9 @@ constexpr std::uint32_t const tfMPTCanEscrow = lsfMPTCanEscrow;
constexpr std::uint32_t const tfMPTCanTrade = lsfMPTCanTrade;
constexpr std::uint32_t const tfMPTCanTransfer = lsfMPTCanTransfer;
constexpr std::uint32_t const tfMPTCanClawback = lsfMPTCanClawback;
constexpr std::uint32_t const tfMPTCanPrivacy = lsfMPTCanPrivacy;
constexpr std::uint32_t const tfMPTokenIssuanceCreateMask =
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback);
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanPrivacy);
// MPTokenIssuanceCreate MutableFlags:
// Indicating specific fields or flags may be changed after issuance.
@@ -145,9 +146,13 @@ constexpr std::uint32_t const tmfMPTCanMutateCanTransfer = lsmfMPTCanMutateCanTr
constexpr std::uint32_t const tmfMPTCanMutateCanClawback = lsmfMPTCanMutateCanClawback;
constexpr std::uint32_t const tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata;
constexpr std::uint32_t const tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee;
// Issuer can mutate lsfMPTPrivacy by default unless lsmfMPTCannotMutatePrivacy is set.
constexpr std::uint32_t const tmfMPTCannotMutatePrivacy = lsmfMPTCannotMutatePrivacy;
constexpr std::uint32_t const tmfMPTokenIssuanceCreateMutableMask =
~(tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow | tmfMPTCanMutateCanTrade
| tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee);
| tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee
| tmfMPTCannotMutatePrivacy);
// MPTokenAuthorize flags:
constexpr std::uint32_t const tfMPTUnauthorize = 0x00000001;
@@ -173,10 +178,12 @@ constexpr std::uint32_t const tmfMPTSetCanTransfer = 0x00000100;
constexpr std::uint32_t const tmfMPTClearCanTransfer = 0x00000200;
constexpr std::uint32_t const tmfMPTSetCanClawback = 0x00000400;
constexpr std::uint32_t const tmfMPTClearCanClawback = 0x00000800;
constexpr std::uint32_t const tmfMPTSetPrivacy = 0x00001000;
constexpr std::uint32_t const tmfMPTClearPrivacy = 0x00002000;
constexpr std::uint32_t const tmfMPTokenIssuanceSetMutableMask = ~(tmfMPTSetCanLock | tmfMPTClearCanLock |
tmfMPTSetRequireAuth | tmfMPTClearRequireAuth | tmfMPTSetCanEscrow | tmfMPTClearCanEscrow |
tmfMPTSetCanTrade | tmfMPTClearCanTrade | tmfMPTSetCanTransfer | tmfMPTClearCanTransfer |
tmfMPTSetCanClawback | tmfMPTClearCanClawback);
tmfMPTSetCanClawback | tmfMPTClearCanClawback | tmfMPTSetPrivacy | tmfMPTClearPrivacy);
// MPTokenIssuanceDestroy flags:
constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal;

View File

@@ -16,6 +16,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(ConfidentialTransfer, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocol, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -398,6 +398,9 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfDomainID, soeOPTIONAL},
{sfMutableFlags, soeDEFAULT},
{sfIssuerElGamalPublicKey, soeOPTIONAL},
{sfAuditorElGamalPublicKey, soeOPTIONAL},
{sfConfidentialOutstandingAmount, soeDEFAULT},
}))
/** A ledger object which tracks MPToken
@@ -411,6 +414,11 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfOwnerNode, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfConfidentialBalanceInbox, soeOPTIONAL},
{sfConfidentialBalanceSpending, soeOPTIONAL},
{sfConfidentialBalanceVersion, soeDEFAULT},
{sfIssuerEncryptedBalance, soeOPTIONAL},
{sfHolderElGamalPublicKey, soeOPTIONAL},
}))
/** A ledger object which tracks Oracle

View File

@@ -114,6 +114,7 @@ TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bi
TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bips)
TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips)
TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips)
TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 69)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -147,6 +148,7 @@ TYPED_SFIELD(sfSubjectNode, UINT64, 28)
TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default)
TYPED_SFIELD(sfVaultNode, UINT64, 30)
TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31)
TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::sMD_BaseTen|SField::sMD_Default)
// 128-bit
TYPED_SFIELD(sfEmailHash, UINT128, 1)
@@ -297,6 +299,19 @@ TYPED_SFIELD(sfAssetClass, VL, 28)
TYPED_SFIELD(sfProvider, VL, 29)
TYPED_SFIELD(sfMPTokenMetadata, VL, 30)
TYPED_SFIELD(sfCredentialType, VL, 31)
TYPED_SFIELD(sfConfidentialBalanceInbox, VL, 32)
TYPED_SFIELD(sfConfidentialBalanceSpending, VL, 33)
TYPED_SFIELD(sfIssuerEncryptedBalance, VL, 34)
TYPED_SFIELD(sfIssuerElGamalPublicKey, VL, 35)
TYPED_SFIELD(sfHolderElGamalPublicKey, VL, 36)
TYPED_SFIELD(sfZKProof, VL, 37)
TYPED_SFIELD(sfHolderEncryptedAmount, VL, 38)
TYPED_SFIELD(sfIssuerEncryptedAmount, VL, 39)
TYPED_SFIELD(sfSenderEncryptedAmount, VL, 40)
TYPED_SFIELD(sfDestinationEncryptedAmount, VL, 41)
TYPED_SFIELD(sfAuditorEncryptedBalance, VL, 42)
TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43)
TYPED_SFIELD(sfAuditorElGamalPublicKey, VL, 44)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)

View File

@@ -722,6 +722,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
{sfMPTokenMetadata, soeOPTIONAL},
{sfTransferFee, soeOPTIONAL},
{sfMutableFlags, soeOPTIONAL},
{sfIssuerElGamalPublicKey, soeOPTIONAL},
}))
/** This transaction type authorizes a MPToken instance */
@@ -1058,6 +1059,82 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay,
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This transaction type converts into confidential MPT balance. */
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialConvert.h>
#endif
TRANSACTION(ttCONFIDENTIAL_CONVERT, 85, ConfidentialConvert,
Delegation::delegatable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeREQUIRED},
{sfHolderElGamalPublicKey, soeOPTIONAL},
{sfHolderEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfZKProof, soeREQUIRED},
}))
/** This transaction type merges MPT inbox. */
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialMergeInbox.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MERGE_INBOX, 86, ConfidentialMergeInbox,
Delegation::delegatable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
}))
/** This transaction type converts back into public MPT balance. */
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialConvertBack.h>
#endif
TRANSACTION(ttCONFIDENTIAL_CONVERT_BACK, 87, ConfidentialConvertBack,
Delegation::delegatable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeREQUIRED},
{sfHolderEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfZKProof, soeREQUIRED},
}))
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialSend.h>
#endif
TRANSACTION(ttCONFIDENTIAL_SEND, 88, ConfidentialSend,
Delegation::delegatable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfDestination, soeREQUIRED},
{sfSenderEncryptedAmount, soeREQUIRED},
{sfDestinationEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfZKProof, soeREQUIRED},
{sfCredentialIDs, soeOPTIONAL},
}))
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialClawback.h>
#endif
TRANSACTION(ttCONFIDENTIAL_CLAWBACK, 89, ConfidentialClawback,
Delegation::delegatable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeREQUIRED},
{sfMPTAmount, soeREQUIRED},
{sfZKProof, soeREQUIRED},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html

View File

@@ -517,7 +517,8 @@ accountHolds(
// Only if auth check is needed, as it needs to do an additional read
// operation. Note featureSingleAssetVault will affect error codes.
if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED &&
view.rules().enabled(featureSingleAssetVault))
(view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureConfidentialTransfer)))
{
if (auto const err =
requireAuth(view, mptIssue, account, AuthType::StrongAuth);

File diff suppressed because it is too large Load Diff

View File

@@ -108,6 +108,7 @@ transResults()
MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."),
MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."),
MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."),
MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
@@ -200,6 +201,7 @@ transResults()
MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."),
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
MAKE_ERROR(terRETRY, "Retry transaction."),
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),

File diff suppressed because it is too large Load Diff

View File

@@ -571,7 +571,8 @@ class MPToken_test : public beast::unit_test::suite
.err = temINVALID_FLAG});
if (!features[featureSingleAssetVault] &&
!features[featureDynamicMPT])
!features[featureDynamicMPT] &&
!features[featureConfidentialTransfer])
{
// test invalid flags - nothing is being changed
mptAlice.set(
@@ -2930,6 +2931,7 @@ class MPToken_test : public beast::unit_test::suite
tmfMPTSetCanTrade | tmfMPTClearCanTrade,
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer,
tmfMPTSetCanClawback | tmfMPTClearCanClawback,
tmfMPTSetPrivacy | tmfMPTClearPrivacy,
tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTClearCanTrade,
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer |
tmfMPTSetCanEscrow | tmfMPTClearCanClawback};

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,8 @@
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
namespace ripple {
namespace test {
namespace jtx {
@@ -16,6 +18,10 @@ class MPTTester;
auto const MPTDEXFlags = tfMPTCanTrade | tfMPTCanTransfer;
// Generates a syntactically valid placeholder ciphertext
ripple::Buffer
generatePlaceholderCiphertext();
// Check flags settings on MPT create
class mptflags
{
@@ -156,6 +162,78 @@ struct MPTSet
std::optional<std::string> metadata = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<uint256> domainID = std::nullopt;
std::optional<Buffer> pubKey = std::nullopt;
std::optional<TER> err = std::nullopt;
};
struct MPTConvert
{
std::optional<Account> account = std::nullopt;
std::optional<MPTID> id = std::nullopt;
std::optional<std::uint64_t> amt = std::nullopt;
std::optional<std::string> proof = std::nullopt;
std::optional<Buffer> holderPubKey = std::nullopt;
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
std::optional<Buffer> auditorEncryptedAmt = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<TER> err = std::nullopt;
};
struct MPTMergeInbox
{
std::optional<Account> account = std::nullopt;
std::optional<MPTID> id = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<TER> err = std::nullopt;
};
struct MPTConfidentialSend
{
std::optional<Account> account = std::nullopt;
std::optional<Account> dest = std::nullopt;
std::optional<MPTID> id = std::nullopt;
// amt is to generate encrypted amounts for testing purposes
std::optional<std::uint64_t> amt = std::nullopt;
std::optional<std::string> proof = std::nullopt;
std::optional<Buffer> senderEncryptedAmt = std::nullopt;
std::optional<Buffer> destEncryptedAmt = std::nullopt;
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
std::optional<std::vector<std::string>> credentials = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<TER> err = std::nullopt;
};
struct MPTConvertBack
{
std::optional<Account> account = std::nullopt;
std::optional<MPTID> id = std::nullopt;
std::optional<std::uint64_t> amt = std::nullopt;
std::optional<std::string> proof = std::nullopt;
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<TER> err = std::nullopt;
};
struct MPTConfidentialClawback
{
std::optional<Account> account = std::nullopt;
std::optional<Account> holder = std::nullopt;
std::optional<MPTID> id = std::nullopt;
std::optional<std::uint64_t> amt = std::nullopt;
std::optional<std::string> proof = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<TER> err = std::nullopt;
};
@@ -166,8 +244,16 @@ class MPTTester
std::unordered_map<std::string, Account> const holders_;
std::optional<MPTID> id_;
bool close_;
std::unordered_map<AccountID, Buffer> pubKeys;
std::unordered_map<AccountID, Buffer> privKeys;
public:
enum EncryptedBalanceType {
ISSUER_ENCRYPTED_BALANCE,
HOLDER_ENCRYPTED_INBOX,
HOLDER_ENCRYPTED_SPENDING,
};
MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {});
MPTTester(MPTInitDef const& constr);
MPTTester(
@@ -205,6 +291,22 @@ public:
static Json::Value
setjv(MPTSet const& set = {});
void
convert(MPTConvert const& arg = MPTConvert{});
void
mergeInbox(MPTMergeInbox const& arg = MPTMergeInbox{});
void
send(MPTConfidentialSend const& arg = MPTConfidentialSend{});
void
convertBack(MPTConvertBack const& arg = MPTConvertBack{});
void
confidentialClaw(
MPTConfidentialClawback const& arg = MPTConfidentialClawback{});
[[nodiscard]] bool
checkDomainID(std::optional<uint256> expected) const;
@@ -215,6 +317,9 @@ public:
[[nodiscard]] bool
checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const;
[[nodiscard]] bool
checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const;
[[nodiscard]] bool
checkFlags(
uint32_t const expectedFlags,
@@ -268,6 +373,14 @@ public:
std::int64_t
getBalance(Account const& account) const;
std::int64_t
getIssuanceConfidentialBalance() const;
std::optional<Buffer>
getEncryptedBalance(
Account const& account,
EncryptedBalanceType option = HOLDER_ENCRYPTED_INBOX) const;
MPT
operator[](std::string const& name) const;
@@ -276,6 +389,48 @@ public:
operator Asset() const;
bool
printMPT(Account const& holder_) const;
void
generateKeyPair(Account const& account);
Buffer
getPubKey(Account const& account) const;
Buffer
getPrivKey(Account const& account) const;
std::pair<Buffer, Buffer>
encryptAmount(Account const& account, uint64_t amt) const;
uint64_t
decryptAmount(Account const& account, Buffer const& amt) const;
uint64_t
getDecryptedBalance(
Account const& account,
EncryptedBalanceType balanceType) const;
std::int64_t
getIssuanceOutstandingBalance() const;
Buffer
getClawbackProof(
Account const& holder,
std::uint64_t amount,
Buffer const& privateKey,
uint256 const& txHash) const;
Buffer
getConvertProof(
Account const& holder,
std::uint64_t amount,
uint256 const& ctxHash,
std::pair<Buffer, Buffer> holderCiphertext,
std::pair<Buffer, Buffer> issuerCiphertext,
std::optional<std::pair<Buffer, Buffer>> auditorCiphertext) const;
private:
using SLEP = SLE::const_pointer;
bool

View File

@@ -0,0 +1,151 @@
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/ConfidentialClawback.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
ConfidentialClawback::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
auto const account = ctx.tx[sfAccount];
auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer();
// Only issuer can clawback
if (account != issuer)
return temMALFORMED;
// Cannot clawback from self
if (account == ctx.tx[sfHolder])
return temMALFORMED;
auto const clawAmount = ctx.tx[sfMPTAmount];
if (clawAmount == 0 || clawAmount > maxMPTokenAmount)
return temBAD_AMOUNT;
if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
return temMALFORMED;
return tesSUCCESS;
}
TER
ConfidentialClawback::preclaim(PreclaimContext const& ctx)
{
// Check if sender account exists
auto const account = ctx.tx[sfAccount];
if (!ctx.view.exists(keylet::account(account)))
return terNO_ACCOUNT;
// Check if holder account exists
auto const holder = ctx.tx[sfHolder];
if (!ctx.view.exists(keylet::account(holder)))
return tecNO_TARGET;
// Check if MPT issuance exists
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// Sanity check: issuer must be the same as account
if (sleIssuance->getAccountID(sfIssuer) != account)
return tefINTERNAL; // LCOV_EXCL_LINE
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;
// Check if clawback is allowed
if (!sleIssuance->isFlag(lsfMPTCanClawback))
return tecNO_PERMISSION;
// Check holder's MPToken
auto const sleHolderMPToken =
ctx.view.read(keylet::mptoken(mptIssuanceID, holder));
if (!sleHolderMPToken)
return tecOBJECT_NOT_FOUND;
// Check if holder has confidential balances to claw back
if (!sleHolderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
return tecNO_PERMISSION;
// Sanity check: claw amount can not exceed confidential outstanding amount
auto const amount = ctx.tx[sfMPTAmount];
if (amount > (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0))
return tecINSUFFICIENT_FUNDS;
auto const ciphertext = (*sleHolderMPToken)[sfIssuerEncryptedBalance];
auto const pubKeySlice = (*sleIssuance)[sfIssuerElGamalPublicKey];
auto const contextHash = getClawbackContextHash(
account, ctx.tx[sfSequence], mptIssuanceID, amount, holder);
return verifyClawbackEqualityProof(
amount, ctx.tx[sfZKProof], pubKeySlice, ciphertext, contextHash);
}
TER
ConfidentialClawback::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto const holder = ctx_.tx[sfHolder];
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder));
if (!sleIssuance || !sleHolderMPToken)
return tecINTERNAL;
auto const clawAmount = ctx_.tx[sfMPTAmount];
Slice const holderPubKey = (*sleHolderMPToken)[sfHolderElGamalPublicKey];
Slice const issuerPubKey = (*sleIssuance)[sfIssuerElGamalPublicKey];
// Encrypt zero amount
Buffer encZeroForHolder;
Buffer encZeroForIssuer;
try
{
encZeroForHolder =
encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
encZeroForIssuer =
encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID);
}
catch (std::exception const& e)
{
JLOG(ctx_.journal.error())
<< "ConfidentialClawback: Failed to generate canonical zero: "
<< e.what();
return tecINTERNAL;
}
// Set holder's confidential balances to encrypted zero
(*sleHolderMPToken)[sfConfidentialBalanceInbox] = encZeroForHolder;
(*sleHolderMPToken)[sfConfidentialBalanceSpending] = encZeroForHolder;
(*sleHolderMPToken)[sfIssuerEncryptedBalance] = encZeroForIssuer;
(*sleHolderMPToken)[sfConfidentialBalanceVersion] = 0;
// Decrease Global Confidential Outstanding Amount
auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount];
(*sleIssuance)[sfConfidentialOutstandingAmount] = oldCOA - clawAmount;
// Decrease Global Total Outstanding Amount
auto const oldOA = (*sleIssuance)[sfOutstandingAmount];
(*sleIssuance)[sfOutstandingAmount] = oldOA - clawAmount;
view().update(sleHolderMPToken);
view().update(sleIssuance);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,29 @@
#ifndef XRPL_TX_CONFIDENTIALCLAWSBACK_H_INCLUDED
#define XRPL_TX_CONFIDENTIALCLAWSBACK_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class ConfidentialClawback : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialClawback(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,243 @@
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/ConfidentialConvert.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
ConfidentialConvert::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot convert
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;
if (ctx.tx[sfHolderEncryptedAmount].length() !=
ecGamalEncryptedTotalLength ||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (ctx.tx[sfMPTAmount] > maxMPTokenAmount)
return temBAD_AMOUNT;
if (!isValidCiphertext(ctx.tx[sfHolderEncryptedAmount]) ||
!isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
return temBAD_CIPHERTEXT;
if (ctx.tx.isFieldPresent(sfHolderElGamalPublicKey) &&
ctx.tx[sfHolderElGamalPublicKey].length() != ecPubKeyLength)
return temMALFORMED;
auto const expectedCount =
ctx.tx.isFieldPresent(sfAuditorEncryptedAmount) ? 3 : 2;
if (ctx.tx[sfZKProof].size() != expectedCount * ecEqualityProofLength)
return temMALFORMED;
return tesSUCCESS;
}
TER
ConfidentialConvert::preclaim(PreclaimContext const& ctx)
{
// ensure that issuance exists
auto const sleIssuance =
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
// issuer has not uploaded their pub key yet
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;
auto const sleMptoken = ctx.view.read(
keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
auto const mptIssue = MPTIssue{ctx.tx[sfMPTokenIssuanceID]};
STAmount const mptAmount = STAmount(
MPTAmount{static_cast<MPTAmount::value_type>(ctx.tx[sfMPTAmount])},
mptIssue);
if (accountHolds(
ctx.view,
ctx.tx[sfAccount],
mptIssue,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,
ctx.j) < mptAmount)
{
return tecINSUFFICIENT_FUNDS;
}
// must have pk to convert
if (!sleMptoken->isFieldPresent(sfHolderElGamalPublicKey) &&
!ctx.tx.isFieldPresent(sfHolderElGamalPublicKey))
return tecNO_PERMISSION;
// can't update if there's already a pk
if (sleMptoken->isFieldPresent(sfHolderElGamalPublicKey) &&
ctx.tx.isFieldPresent(sfHolderElGamalPublicKey))
return tecDUPLICATE;
auto const holderPubKey = ctx.tx.isFieldPresent(sfHolderElGamalPublicKey)
? ctx.tx[sfHolderElGamalPublicKey]
: (*sleMptoken)[sfHolderElGamalPublicKey];
auto const contextHash = getConvertContextHash(
ctx.tx[sfAccount],
ctx.tx[sfSequence],
ctx.tx[sfMPTokenIssuanceID],
ctx.tx[sfMPTAmount]);
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
std::vector<Buffer> const zkps = getEqualityProofs(ctx.tx[sfZKProof]);
auto const& amount = ctx.tx[sfMPTAmount];
// we already checked proof size in preflight, still do sanity check here
// since we are going to access individual vector entries
auto const expectedCount = ctx.tx[sfZKProof].size() / ecEqualityProofLength;
if (zkps.size() != expectedCount)
return tecINTERNAL; // LCOV_EXCL_LINE
// check equality proof
if (!isTesSuccess(verifyEqualityProof(
amount,
zkps[0],
holderPubKey,
ctx.tx[sfHolderEncryptedAmount],
contextHash)) ||
!isTesSuccess(verifyEqualityProof(
amount,
zkps[1],
(*sleIssuance)[sfIssuerElGamalPublicKey],
ctx.tx[sfIssuerEncryptedAmount],
contextHash)))
{
return tecBAD_PROOF;
}
// Verify Auditor proof if present
if (hasAuditor &&
!isTesSuccess(verifyEqualityProof(
amount,
zkps[2],
(*sleIssuance)[sfAuditorElGamalPublicKey],
ctx.tx[sfAuditorEncryptedAmount],
contextHash)))
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
ConfidentialConvert::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
if (!sleMptoken)
return tecINTERNAL;
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL;
auto const amtToConvert = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
if (ctx_.tx.isFieldPresent(sfHolderElGamalPublicKey))
(*sleMptoken)[sfHolderElGamalPublicKey] =
ctx_.tx[sfHolderElGamalPublicKey];
(*sleMptoken)[sfMPTAmount] = amt - amtToConvert;
(*sleIssuance)[sfConfidentialOutstandingAmount] =
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) +
amtToConvert;
Slice const holderEc = ctx_.tx[sfHolderEncryptedAmount];
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
// todo: we should check sfConfidentialBalanceSpending depending on if we
// encrypt zero amount
if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
{
// homomorphically add holder's encrypted balance
{
Buffer sum(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(
holderEc, (*sleMptoken)[sfConfidentialBalanceInbox], sum);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleMptoken)[sfConfidentialBalanceInbox] = sum;
}
// homomorphically add issuer's encrypted balance
{
Buffer sum(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(
issuerEc, (*sleMptoken)[sfIssuerEncryptedBalance], sum);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleMptoken)[sfIssuerEncryptedBalance] = sum;
}
}
else if (
!sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
{
(*sleMptoken)[sfConfidentialBalanceInbox] = holderEc;
(*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc;
(*sleMptoken)[sfConfidentialBalanceVersion] = 0;
try
{
// encrypt sfConfidentialBalanceSpending with zero balance
Buffer out;
out =
encryptAmount(0, (*sleMptoken)[sfHolderElGamalPublicKey]).first;
(*sleMptoken)[sfConfidentialBalanceSpending] = out;
}
catch (std::exception const& e)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
}
else
{
// both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
// exist together
return tecINTERNAL; // LCOV_EXCL_LINE
}
view().update(sleIssuance);
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,29 @@
#ifndef XRPL_TX_CONFIDENTIALCONVERT_H_INCLUDED
#define XRPL_TX_CONFIDENTIALCONVERT_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class ConfidentialConvert : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialConvert(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,178 @@
#include <xrpld/app/tx/detail/ConfidentialConvertBack.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
ConfidentialConvertBack::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot convert
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;
if (ctx.tx[sfHolderEncryptedAmount].length() !=
ecGamalEncryptedTotalLength ||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > maxMPTokenAmount)
return temBAD_AMOUNT;
if (!isValidCiphertext(ctx.tx[sfHolderEncryptedAmount]) ||
!isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
return temBAD_CIPHERTEXT;
// todo: update with correct size of proof since it might also contain range
// proof
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
// return temMALFORMED;
return tesSUCCESS;
}
TER
ConfidentialConvertBack::preclaim(PreclaimContext const& ctx)
{
// ensure that issuance exists
auto const sleIssuance =
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleMptoken = ctx.view.read(
keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfHolderElGamalPublicKey))
{
return tecNO_PERMISSION;
}
// if the total circulating confidential balance is smaller than what the
// holder is trying to convert back, we know for sure this txn should
// fail
if ((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) <
ctx.tx[sfMPTAmount])
{
return tecINSUFFICIENT_FUNDS;
}
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const account = ctx.tx[sfAccount];
// Check lock
MPTIssue const mptIssue(mptIssuanceID);
if (auto const ter = checkFrozen(ctx.view, account, mptIssue);
!isTesSuccess(ter))
return ter;
// Check auth
if (auto const ter = requireAuth(ctx.view, mptIssue, account);
!isTesSuccess(ter))
return ter;
// todo: need addtional parsing, the proof should contain multiple proofs
// auto checkEqualityProof = [&](auto const& encryptedAmount,
// auto const& pubKey) -> TER {
// return proveEquality(
// ctx.tx[sfZKProof],
// encryptedAmount,
// pubKey,
// ctx.tx[sfMPTAmount],
// ctx.tx.getTransactionID(),
// (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0));
// };
// if (!isTesSuccess(checkEqualityProof(
// ctx.tx[sfHolderEncryptedAmount],
// (*sleMptoken)[sfHolderElGamalPublicKey])) ||
// !isTesSuccess(checkEqualityProof(
// ctx.tx[sfIssuerEncryptedAmount],
// (*sleIssuance)[sfIssuerElGamalPublicKey])))
// {
// return tecBAD_PROOF;
// }
// todo: also check range proof that
// sfHolderEncryptedAmount <= sfConfidentialBalanceSpending AND
// sfIssuerEncryptedAmount <= sfIssuerEncryptedBalance
return tesSUCCESS;
}
TER
ConfidentialConvertBack::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
if (!sleMptoken)
return tecINTERNAL;
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL;
auto const amtToConvertBack = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
(*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack;
(*sleIssuance)[sfConfidentialOutstandingAmount] =
(*sleIssuance)[sfConfidentialOutstandingAmount] - amtToConvertBack;
// it's fine if it reaches max uint32, it just resets to 0
(*sleMptoken)[sfConfidentialBalanceVersion] =
(*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
// homomorphically subtract holder's encrypted balance
{
Buffer res(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicSubtract(
(*sleMptoken)[sfConfidentialBalanceSpending],
ctx_.tx[sfHolderEncryptedAmount],
res);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleMptoken)[sfConfidentialBalanceSpending] = res;
}
// homomorphically subtract issuer's encrypted balance
{
Buffer res(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicSubtract(
(*sleMptoken)[sfIssuerEncryptedBalance],
ctx_.tx[sfIssuerEncryptedAmount],
res);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleMptoken)[sfIssuerEncryptedBalance] = res;
}
view().update(sleIssuance);
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,29 @@
#ifndef XRPL_TX_CONFIDENTIALCONVERTBACK_H_INCLUDED
#define XRPL_TX_CONFIDENTIALCONVERTBACK_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class ConfidentialConvertBack : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialConvertBack(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,92 @@
#include <xrpld/app/tx/detail/ConfidentialMergeInbox.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
ConfidentialMergeInbox::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot merge
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;
return tesSUCCESS;
}
TER
ConfidentialMergeInbox::preclaim(PreclaimContext const& ctx)
{
auto const sleIssuance =
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleMptoken = ctx.view.read(
keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
ConfidentialMergeInbox::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
if (!sleMptoken)
return tecINTERNAL;
// homomorphically add holder's encrypted balance
Buffer sum(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(
(*sleMptoken)[sfConfidentialBalanceSpending],
(*sleMptoken)[sfConfidentialBalanceInbox],
sum);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleMptoken)[sfConfidentialBalanceSpending] = sum;
try
{
Buffer zeroEncyption;
zeroEncyption = encryptCanonicalZeroAmount(
(*sleMptoken)[sfHolderElGamalPublicKey], account_, mptIssuanceID);
(*sleMptoken)[sfConfidentialBalanceInbox] = zeroEncyption;
}
catch (std::exception const& e)
{
return tecINTERNAL;
}
// it's fine if it reaches max uint32, it just resets to 0
(*sleMptoken)[sfConfidentialBalanceVersion] =
(*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,29 @@
#ifndef XRPL_TX_CONFIDENTIALMERGEINBOX_H_INCLUDED
#define XRPL_TX_CONFIDENTIALMERGEINBOX_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class ConfidentialMergeInbox : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMergeInbox(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,238 @@
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/ConfidentialSend.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
ConfidentialSend::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
auto const account = ctx.tx[sfAccount];
auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer();
// ConfidentialSend only allows holder to holder, holder to second account,
// and second account to holder transfers. So issuer cannot be the sender.
if (account == issuer)
return temMALFORMED;
// Can not send to self
if (account == ctx.tx[sfDestination])
return temMALFORMED;
if (ctx.tx[sfSenderEncryptedAmount].length() !=
ecGamalEncryptedTotalLength ||
ctx.tx[sfDestinationEncryptedAmount].length() !=
ecGamalEncryptedTotalLength ||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (!isValidCiphertext(ctx.tx[sfSenderEncryptedAmount]) ||
!isValidCiphertext(ctx.tx[sfDestinationEncryptedAmount]) ||
!isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
return temBAD_CIPHERTEXT;
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
// return temMALFORMED;
return tesSUCCESS;
}
TER
ConfidentialSend::preclaim(PreclaimContext const& ctx)
{
// Check if sender account exists
auto const account = ctx.tx[sfAccount];
if (!ctx.view.exists(keylet::account(account)))
return terNO_ACCOUNT;
// Check if destination account exists
auto const destination = ctx.tx[sfDestination];
if (!ctx.view.exists(keylet::account(destination)))
return tecNO_TARGET;
// Check if MPT issuance exists
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// Check if the issuance allows transfer
if (!sleIssuance->isFlag(lsfMPTCanTransfer))
return tecNO_AUTH;
// Check if issuance allows confidential transfer
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
return tecNO_PERMISSION;
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
// Check sender's MPToken
auto const sleSenderMPToken =
ctx.view.read(keylet::mptoken(mptIssuanceID, account));
if (!sleSenderMPToken)
return tecOBJECT_NOT_FOUND;
// Check sender's MPToken has necessary fields for confidential send
if (!sleSenderMPToken->isFieldPresent(sfHolderElGamalPublicKey) ||
!sleSenderMPToken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleSenderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
return tecNO_PERMISSION;
// Check destination's MPToken
auto const sleDestinationMPToken =
ctx.view.read(keylet::mptoken(mptIssuanceID, destination));
if (!sleDestinationMPToken)
return tecOBJECT_NOT_FOUND;
// Check destination's MPToken has necessary fields for confidential send
if (!sleDestinationMPToken->isFieldPresent(sfHolderElGamalPublicKey) ||
!sleDestinationMPToken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleDestinationMPToken->isFieldPresent(sfIssuerEncryptedBalance))
return tecNO_PERMISSION;
// Check lock
MPTIssue const mptIssue(mptIssuanceID);
if (auto const ter = checkFrozen(ctx.view, account, mptIssue);
!isTesSuccess(ter))
return ter;
if (auto const ter = checkFrozen(ctx.view, destination, mptIssue);
!isTesSuccess(ter))
return ter;
// Check auth
if (auto const ter = requireAuth(ctx.view, mptIssue, account);
!isTesSuccess(ter))
return ter;
if (auto const ter = requireAuth(ctx.view, mptIssue, destination);
!isTesSuccess(ter))
return ter;
// todo: check zkproof. equality proof and range proof, combined or separate
// TBD. TER const terProof = verifyConfidentialSendProof(
// ctx.tx[sfZKProof],
// (*sleSender)[sfConfidentialBalanceSpending],
// ctx.tx[sfSenderEncryptedAmount],
// ctx.tx[sfDestinationEncryptedAmount],
// ctx.tx[sfIssuerEncryptedAmount],
// (*sleSender)[sfHolderElGamalPublicKey],
// (*sleDestination)[sfHolderElGamalPublicKey],
// (*sleIssuance)[sfIssuerElGamalPublicKey],
// (*sleSender)[~sfConfidentialBalanceVersion].value_or(0),
// ctx.tx.getTransactionID()
// );
// if (!isTesSuccess(terProof))
// return tecBAD_PROOF;
return tesSUCCESS;
}
TER
ConfidentialSend::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto const destination = ctx_.tx[sfDestination];
auto sleSender = view().peek(keylet::mptoken(mptIssuanceID, account_));
auto sleDestination =
view().peek(keylet::mptoken(mptIssuanceID, destination));
auto sleDestAcct = view().peek(keylet::account(destination));
if (!sleSender || !sleDestination || !sleDestAcct)
return tecINTERNAL;
if (auto err = verifyDepositPreauth(
ctx_.tx,
ctx_.view(),
account_,
destination,
sleDestAcct,
ctx_.journal);
!isTesSuccess(err))
return err;
Slice const senderEc = ctx_.tx[sfSenderEncryptedAmount];
Slice const destEc = ctx_.tx[sfDestinationEncryptedAmount];
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
// Subtract from sender's spending balance
{
Slice const curSpending = (*sleSender)[sfConfidentialBalanceSpending];
Buffer newSpending(ecGamalEncryptedTotalLength);
if (TER const ter =
homomorphicSubtract(curSpending, senderEc, newSpending);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleSender)[sfConfidentialBalanceSpending] = newSpending;
}
// Subtract from issuer's balance
{
Slice const curIssuerEnc = (*sleSender)[sfIssuerEncryptedBalance];
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
if (TER const ter =
homomorphicSubtract(curIssuerEnc, issuerEc, newIssuerEnc);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleSender)[sfIssuerEncryptedBalance] = newIssuerEnc;
}
// Increment version
(*sleSender)[sfConfidentialBalanceVersion] =
(*sleSender)[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
// Add to destination's inbox balance
{
Slice const curInbox = (*sleDestination)[sfConfidentialBalanceInbox];
Buffer newInbox(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(curInbox, destEc, newInbox);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleDestination)[sfConfidentialBalanceInbox] = newInbox;
}
// Add to issuer's balance
{
Slice const curIssuerEnc = (*sleDestination)[sfIssuerEncryptedBalance];
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
if (TER const ter =
homomorphicAdd(curIssuerEnc, issuerEc, newIssuerEnc);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleDestination)[sfIssuerEncryptedBalance] = newIssuerEnc;
}
view().update(sleSender);
view().update(sleDestination);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,29 @@
#ifndef XRPL_TX_CONFIDENTIALSEND_H_INCLUDED
#define XRPL_TX_CONFIDENTIALSEND_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class ConfidentialSend : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialSend(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -74,6 +74,26 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
sleMpt->isFlag(lsfMPTLocked))
return tecNO_PERMISSION;
if (ctx.view.rules().enabled(featureConfidentialTransfer))
{
auto const sleMptIssuance = ctx.view.read(
keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
// if there still existing encrypted balances of MPT in
// circulation
if (sleMptIssuance &&
(*sleMptIssuance)[~sfConfidentialOutstandingAmount]
.value_or(0) != 0)
{
// this MPT still has encrypted balance, since we don't know
// if it's non-zero or not, we won't allow deletion of
// MPToken
if (sleMpt->isFieldPresent(sfConfidentialBalanceInbox) ||
sleMpt->isFieldPresent(sfConfidentialBalanceSpending))
return tecHAS_OBLIGATIONS;
}
}
return tesSUCCESS;
}

View File

@@ -18,6 +18,16 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
!ctx.rules.enabled(featureDynamicMPT))
return false;
if (ctx.tx.isFlag(tfMPTCanPrivacy) &&
!ctx.rules.enabled(featureConfidentialTransfer))
return false;
// can not set tmfMPTCannotMutatePrivacy without featureConfidentialTransfer
auto const mutableFlags = ctx.tx[~sfMutableFlags];
if (mutableFlags && (*mutableFlags & tmfMPTCannotMutatePrivacy) &&
!ctx.rules.enabled(featureConfidentialTransfer))
return false;
return true;
}

View File

@@ -28,22 +28,41 @@ struct MPTMutabilityFlags
{
std::uint32_t setFlag;
std::uint32_t clearFlag;
std::uint32_t canMutateFlag;
std::uint32_t mutabilityFlag;
std::uint32_t targetFlag;
bool isCannotMutate = false; // if true, cannot mutate by default.
};
static constexpr std::array<MPTMutabilityFlags, 6> mptMutabilityFlags = {
{{tmfMPTSetCanLock, tmfMPTClearCanLock, lsmfMPTCanMutateCanLock},
static constexpr std::array<MPTMutabilityFlags, 7> mptMutabilityFlags = {
{{tmfMPTSetCanLock,
tmfMPTClearCanLock,
lsmfMPTCanMutateCanLock,
lsfMPTCanLock},
{tmfMPTSetRequireAuth,
tmfMPTClearRequireAuth,
lsmfMPTCanMutateRequireAuth},
{tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, lsmfMPTCanMutateCanEscrow},
{tmfMPTSetCanTrade, tmfMPTClearCanTrade, lsmfMPTCanMutateCanTrade},
lsmfMPTCanMutateRequireAuth,
lsfMPTRequireAuth},
{tmfMPTSetCanEscrow,
tmfMPTClearCanEscrow,
lsmfMPTCanMutateCanEscrow,
lsfMPTCanEscrow},
{tmfMPTSetCanTrade,
tmfMPTClearCanTrade,
lsmfMPTCanMutateCanTrade,
lsfMPTCanTrade},
{tmfMPTSetCanTransfer,
tmfMPTClearCanTransfer,
lsmfMPTCanMutateCanTransfer},
lsmfMPTCanMutateCanTransfer,
lsfMPTCanTransfer},
{tmfMPTSetCanClawback,
tmfMPTClearCanClawback,
lsmfMPTCanMutateCanClawback}}};
lsmfMPTCanMutateCanClawback,
lsfMPTCanClawback},
{tmfMPTSetPrivacy,
tmfMPTClearPrivacy,
lsmfMPTCannotMutatePrivacy,
lsfMPTCanPrivacy,
true}}};
NotTEC
MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
@@ -52,14 +71,34 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
auto const metadata = ctx.tx[~sfMPTokenMetadata];
auto const transferFee = ctx.tx[~sfTransferFee];
auto const isMutate = mutableFlags || metadata || transferFee;
auto const hasElGamalKey = ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey);
auto const txFlags = ctx.tx.getFlags();
auto const mutatePrivacy = mutableFlags &&
((*mutableFlags & (tmfMPTSetPrivacy | tmfMPTClearPrivacy)));
auto const hasDomain = ctx.tx.isFieldPresent(sfDomainID);
auto const hasHolder = ctx.tx.isFieldPresent(sfHolder);
if (isMutate && !ctx.rules.enabled(featureDynamicMPT))
return temDISABLED;
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
if ((hasElGamalKey || mutatePrivacy) &&
!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
if (hasDomain && hasHolder)
return temMALFORMED;
auto const txFlags = ctx.tx.getFlags();
if (mutatePrivacy && hasHolder)
return temMALFORMED;
if (hasElGamalKey && hasHolder)
return temMALFORMED;
if (hasElGamalKey &&
ctx.tx[sfIssuerElGamalPublicKey].length() != ecPubKeyLength)
return temMALFORMED;
// fails if both flags are set
if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock))
@@ -71,10 +110,11 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
return temMALFORMED;
if (ctx.rules.enabled(featureSingleAssetVault) ||
ctx.rules.enabled(featureDynamicMPT))
ctx.rules.enabled(featureDynamicMPT) ||
ctx.rules.enabled(featureConfidentialTransfer))
{
// Is this transaction actually changing anything ?
if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate)
if (txFlags == 0 && !hasDomain && !hasElGamalKey && !isMutate)
return temMALFORMED;
}
@@ -216,16 +256,32 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return currentMutableFlags & mutableFlag;
};
if (auto const mutableFlags = ctx.tx[~sfMutableFlags])
auto const mutableFlags = ctx.tx[~sfMutableFlags];
if (mutableFlags)
{
if (std::any_of(
mptMutabilityFlags.begin(),
mptMutabilityFlags.end(),
[mutableFlags, &isMutableFlag](auto const& f) {
return !isMutableFlag(f.canMutateFlag) &&
((*mutableFlags & (f.setFlag | f.clearFlag)));
bool const canMutate = f.isCannotMutate
? isMutableFlag(f.mutabilityFlag)
: !isMutableFlag(f.mutabilityFlag);
return canMutate &&
(*mutableFlags & (f.setFlag | f.clearFlag));
}))
return tecNO_PERMISSION;
if ((*mutableFlags & tmfMPTSetPrivacy) ||
(*mutableFlags & tmfMPTClearPrivacy))
{
std::uint64_t const confidentialOA =
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0);
// If there's any confidential outstanding amount, disallow toggling
// the lsfMPTCanPrivacy flag
if (confidentialOA > 0)
return tecNO_PERMISSION;
}
}
if (!isMutableFlag(lsmfMPTCanMutateMetadata) &&
@@ -245,6 +301,19 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
// cannot update public key
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) &&
sleMptIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
{
return tecNO_PERMISSION;
}
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) &&
!sleMptIssuance->isFlag(lsfMPTCanPrivacy))
{
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
@@ -278,9 +347,9 @@ MPTokenIssuanceSet::doApply()
for (auto const& f : mptMutabilityFlags)
{
if (mutableFlags & f.setFlag)
flagsOut |= f.canMutateFlag;
flagsOut |= f.targetFlag;
else if (mutableFlags & f.clearFlag)
flagsOut &= ~f.canMutateFlag;
flagsOut &= ~f.targetFlag;
}
if (mutableFlags & tmfMPTClearCanTransfer)
@@ -332,6 +401,16 @@ MPTokenIssuanceSet::doApply()
}
}
if (auto const pubKey = ctx_.tx[~sfIssuerElGamalPublicKey])
{
// This is enforced in preflight.
XRPL_ASSERT(
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
sle->setFieldVL(sfIssuerElGamalPublicKey, *pubKey);
}
view().update(sle);
return tesSUCCESS;