Compare commits

...

28 Commits

Author SHA1 Message Date
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
28 changed files with 5440 additions and 54 deletions

View File

@@ -0,0 +1,180 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_PROTOCOL_CONFIDENTIALTRANSFER_H_INCLUDED
#define RIPPLE_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/detail/secp256k1.h>
#include <secp256k1.h>
namespace ripple {
/**
* @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
);
// 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);
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);
} // namespace ripple
#endif

View File

@@ -187,6 +187,7 @@ enum LedgerSpecificFlags {
lsfMPTCanTrade = 0x00000010,
lsfMPTCanTransfer = 0x00000020,
lsfMPTCanClawback = 0x00000040,
lsfMPTNoConfidentialTransfer = 0x00000080,
lsmfMPTCanMutateCanLock = 0x00000002,
lsmfMPTCanMutateRequireAuth = 0x00000004,

View File

@@ -181,6 +181,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

@@ -141,6 +141,7 @@ enum TEMcodes : TERUnderlyingType {
temARRAY_TOO_LARGE,
temBAD_TRANSFER_FEE,
temINVALID_INNER_BATCH,
temBAD_CIPHERTEXT,
};
//------------------------------------------------------------------------------
@@ -366,6 +367,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

@@ -151,8 +151,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 tfMPTNoConfidentialTransfer = lsfMPTNoConfidentialTransfer;
constexpr std::uint32_t const tfMPTokenIssuanceCreateMask =
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback);
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback | tfMPTNoConfidentialTransfer);
// MPTokenIssuanceCreate MutableFlags:
// Indicating specific fields or flags may be changed after issuance.

View File

@@ -30,6 +30,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(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (IncludeKeyletFields, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -416,6 +416,8 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfDomainID, soeOPTIONAL},
{sfMutableFlags, soeDEFAULT},
{sfIssuerElGamalPublicKey, soeOPTIONAL},
{sfConfidentialOutstandingAmount, soeDEFAULT},
}))
/** A ledger object which tracks MPToken
@@ -429,6 +431,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

@@ -115,6 +115,7 @@ TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
TYPED_SFIELD(sfMutableFlags, UINT32, 53)
TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 54)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -146,6 +147,7 @@ TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SFie
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default)
TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 30, SField::sMD_BaseTen|SField::sMD_Default)
// 128-bit
TYPED_SFIELD(sfEmailHash, UINT128, 1)
@@ -284,6 +286,16 @@ 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)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)

View File

@@ -741,6 +741,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
{sfMPTokenMetadata, soeOPTIONAL},
{sfTransferFee, soeOPTIONAL},
{sfMutableFlags, soeOPTIONAL},
{sfIssuerElGamalPublicKey, soeOPTIONAL},
}))
/** This transaction type authorizes a MPToken instance */
@@ -944,6 +945,82 @@ TRANSACTION(ttBATCH, 71, Batch,
{sfBatchSigners, soeOPTIONAL},
}))
/** This transaction type converts into confidential MPT balance. */
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialConvert.h>
#endif
TRANSACTION(ttCONFIDENTIAL_CONVERT, 72, 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, 73, 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, 74, 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, 75, 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, 76, 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

@@ -504,7 +504,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);

View File

@@ -0,0 +1,607 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
namespace ripple {
int
secp256k1_elgamal_generate_keypair(
secp256k1_context const* ctx,
unsigned char* privkey,
secp256k1_pubkey* pubkey)
{
// 1. Generate 32 random bytes for the private key
do
{
if (RAND_bytes(privkey, 32) != 1)
{
return 0; // Failure
}
// 2. Verify the random data is a valid private key.
} while (secp256k1_ec_seckey_verify(ctx, privkey) != 1);
// 3. Create the corresponding public key.
if (secp256k1_ec_pubkey_create(ctx, pubkey, privkey) != 1)
{
return 0; // Failure
}
return 1; // Success
}
// ... implementation of secp256k1_elgamal_encrypt ...
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)
{
secp256k1_pubkey S;
// First, calculate C1 = k * G
if (secp256k1_ec_pubkey_create(ctx, c1, blinding_factor) != 1)
{
return 0;
}
// Next, calculate the shared secret S = k * Q
S = *pubkey_Q;
if (secp256k1_ec_pubkey_tweak_mul(ctx, &S, blinding_factor) != 1)
{
return 0;
}
// --- Handle the amount ---
if (amount == 0)
{
// For amount = 0, C2 = S.
*c2 = S;
}
else
{
// For non-zero amounts, proceed as before.
unsigned char amount_scalar[32] = {0};
secp256k1_pubkey M;
secp256k1_pubkey const* points_to_add[2];
// Convert amount to a 32-byte BIG-ENDIAN scalar.
for (int i = 0; i < 8; ++i)
{
amount_scalar[31 - i] = (amount >> (i * 8)) & 0xFF;
}
// Calculate M = amount * G
if (secp256k1_ec_pubkey_create(ctx, &M, amount_scalar) != 1)
{
return 0;
}
// Calculate C2 = M + S
points_to_add[0] = &M;
points_to_add[1] = &S;
if (secp256k1_ec_pubkey_combine(ctx, c2, points_to_add, 2) != 1)
{
return 0;
}
}
return 1; // Success
}
// ... implementation of secp256k1_elgamal_decrypt ...
int
secp256k1_elgamal_decrypt(
secp256k1_context const* ctx,
uint64_t* amount,
secp256k1_pubkey const* c1,
secp256k1_pubkey const* c2,
unsigned char const* privkey)
{
secp256k1_pubkey S, M, G_point, current_M, next_M;
secp256k1_pubkey const* points_to_add[2];
unsigned char c2_bytes[33], s_bytes[33], m_bytes[33], current_m_bytes[33];
size_t len;
uint64_t i;
/* Create the scalar '1' in big-endian format */
unsigned char one_scalar[32] = {0};
one_scalar[31] = 1;
/* --- Executable Code --- */
// 1. Calculate S = privkey * C1
S = *c1;
if (secp256k1_ec_pubkey_tweak_mul(ctx, &S, privkey) != 1)
{
return 0;
}
// 2. Check for amount = 0 by comparing serialized points
len = sizeof(c2_bytes);
if (secp256k1_ec_pubkey_serialize(
ctx, c2_bytes, &len, c2, SECP256K1_EC_COMPRESSED) != 1)
return 0;
len = sizeof(s_bytes);
if (secp256k1_ec_pubkey_serialize(
ctx, s_bytes, &len, &S, SECP256K1_EC_COMPRESSED) != 1)
return 0;
if (memcmp(c2_bytes, s_bytes, sizeof(c2_bytes)) == 0)
{
*amount = 0;
return 1;
}
// 3. Recover M = C2 - S
if (secp256k1_ec_pubkey_negate(ctx, &S) != 1)
return 0;
points_to_add[0] = c2;
points_to_add[1] = &S;
if (secp256k1_ec_pubkey_combine(ctx, &M, points_to_add, 2) != 1)
{
return 0;
}
// 4. Serialize M once for comparison in the loop
len = sizeof(m_bytes);
if (secp256k1_ec_pubkey_serialize(
ctx, m_bytes, &len, &M, SECP256K1_EC_COMPRESSED) != 1)
return 0;
// 5. Brute-force search loop
if (secp256k1_ec_pubkey_create(ctx, &G_point, one_scalar) != 1)
return 0;
current_M = G_point;
for (i = 1; i <= 1000000; ++i)
{
len = sizeof(current_m_bytes);
if (secp256k1_ec_pubkey_serialize(
ctx,
current_m_bytes,
&len,
&current_M,
SECP256K1_EC_COMPRESSED) != 1)
return 0;
if (memcmp(m_bytes, current_m_bytes, sizeof(m_bytes)) == 0)
{
*amount = i;
return 1;
}
points_to_add[0] = &current_M;
points_to_add[1] = &G_point;
if (secp256k1_ec_pubkey_combine(ctx, &next_M, points_to_add, 2) != 1)
return 0;
current_M = next_M;
}
return 0; // Not found
}
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)
{
secp256k1_pubkey const* c1_points[2] = {a_c1, b_c1};
if (secp256k1_ec_pubkey_combine(ctx, sum_c1, c1_points, 2) != 1)
{
return 0;
}
secp256k1_pubkey const* c2_points[2] = {a_c2, b_c2};
if (secp256k1_ec_pubkey_combine(ctx, sum_c2, c2_points, 2) != 1)
{
return 0;
}
return 1;
}
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)
{
// To subtract, we add the negation: (A - B) is (A + (-B))
// Make a local, modifiable copy of B's points.
secp256k1_pubkey neg_b_c1 = *b_c1;
secp256k1_pubkey neg_b_c2 = *b_c2;
// Negate the copies
if (secp256k1_ec_pubkey_negate(ctx, &neg_b_c1) != 1 ||
secp256k1_ec_pubkey_negate(ctx, &neg_b_c2) != 1)
{
return 0; // Negation failed
}
// Now, add A and the negated copies of B
secp256k1_pubkey const* c1_points[2] = {a_c1, &neg_b_c1};
if (secp256k1_ec_pubkey_combine(ctx, diff_c1, c1_points, 2) != 1)
{
return 0;
}
secp256k1_pubkey const* c2_points[2] = {a_c2, &neg_b_c2};
if (secp256k1_ec_pubkey_combine(ctx, diff_c2, c2_points, 2) != 1)
{
return 0;
}
return 1; // Success
}
// Helper function to concatenate data for hashing
static void
build_hash_input(
unsigned char* output_buffer,
size_t buffer_size,
unsigned char const* account_id, // 20 bytes
unsigned char const* mpt_issuance_id // 24 bytes
)
{
char const* domain_separator = "EncZero";
size_t domain_len = strlen(domain_separator);
size_t offset = 0;
// Ensure buffer is large enough (should be checked by caller if necessary)
// Size = strlen("EncZero") + 20 + 24 = 7 + 20 + 24 = 51 bytes
memcpy(output_buffer + offset, domain_separator, domain_len);
offset += domain_len;
memcpy(output_buffer + offset, account_id, 20);
offset += 20;
memcpy(output_buffer + offset, mpt_issuance_id, 24);
// offset += 24; // Final size is offset + 24
}
// The canonical encrypted zero
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
)
{
unsigned char deterministic_scalar[32];
unsigned char hash_input[51]; // Size calculated above
/* 1. Create the input buffer for hashing */
build_hash_input(
hash_input, sizeof(hash_input), account_id, mpt_issuance_id);
/* 2. Hash the buffer to create the deterministic scalar 'r' */
do
{
// Hash the concatenated bytes
SHA256(hash_input, sizeof(hash_input), deterministic_scalar);
/* Note: If the hash output could be invalid (0 or >= n),
* you might need to add a nonce/counter to hash_input
* and re-hash in a loop until a valid scalar is produced. */
} while (secp256k1_ec_seckey_verify(ctx, deterministic_scalar) != 1);
/* 3. Encrypt the amount 0 using the deterministic scalar */
return secp256k1_elgamal_encrypt(
ctx,
enc_zero_c1,
enc_zero_c2,
pubkey,
0, /* The amount is zero */
deterministic_scalar);
}
bool
makeEcPair(Slice const& buffer, secp256k1_pubkey& out1, secp256k1_pubkey& out2)
{
auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) {
return secp256k1_ec_pubkey_parse(
secp256k1Context(),
&out,
reinterpret_cast<unsigned char const*>(slice.data()),
slice.length());
};
Slice s1{buffer.data(), ecGamalEncryptedLength};
Slice s2{buffer.data() + ecGamalEncryptedLength, ecGamalEncryptedLength};
int const ret1 = parsePubKey(s1, out1);
int const ret2 = parsePubKey(s2, out2);
return ret1 == 1 && ret2 == 1;
}
bool
serializeEcPair(
secp256k1_pubkey const& in1,
secp256k1_pubkey const& in2,
Buffer& buffer)
{
auto serializePubKey = [](secp256k1_pubkey const& pub, unsigned char* out) {
size_t outLen = ecGamalEncryptedLength; // 33 bytes
int const ret = secp256k1_ec_pubkey_serialize(
secp256k1Context(), out, &outLen, &pub, SECP256K1_EC_COMPRESSED);
return ret == 1 && outLen == ecGamalEncryptedLength;
};
unsigned char* ptr = buffer.data();
bool const res1 = serializePubKey(in1, ptr);
bool const res2 = serializePubKey(in2, ptr + ecGamalEncryptedLength);
return res1 && res2;
}
bool
isValidCiphertext(Slice const& buffer)
{
// Local/temporary variables to pass to makeEcPair.
// Their contents will be discarded when the function returns.
secp256k1_pubkey key1;
secp256k1_pubkey key2;
// Call makeEcPair and return its result.
return makeEcPair(buffer, key1, key2);
}
TER
homomorphicAdd(Slice const& a, Slice const& b, Buffer& out)
{
if (a.length() != ecGamalEncryptedTotalLength ||
b.length() != ecGamalEncryptedTotalLength)
return tecINTERNAL;
secp256k1_pubkey aC1;
secp256k1_pubkey aC2;
secp256k1_pubkey bC1;
secp256k1_pubkey bC2;
if (!makeEcPair(a, aC1, aC2) || !makeEcPair(b, bC1, bC2))
return tecINTERNAL;
secp256k1_pubkey sumC1;
secp256k1_pubkey sumC2;
if (secp256k1_elgamal_add(
secp256k1Context(), &sumC1, &sumC2, &aC1, &aC2, &bC1, &bC2) != 1)
return tecINTERNAL;
if (!serializeEcPair(sumC1, sumC2, out))
return tecINTERNAL;
return tesSUCCESS;
}
TER
homomorphicSubtract(Slice const& a, Slice const& b, Buffer& out)
{
if (a.length() != ecGamalEncryptedTotalLength ||
b.length() != ecGamalEncryptedTotalLength)
return tecINTERNAL;
secp256k1_pubkey aC1;
secp256k1_pubkey aC2;
secp256k1_pubkey bC1;
secp256k1_pubkey bC2;
if (!makeEcPair(a, aC1, aC2) || !makeEcPair(b, bC1, bC2))
return tecINTERNAL;
secp256k1_pubkey diffC1;
secp256k1_pubkey diffC2;
if (secp256k1_elgamal_subtract(
secp256k1Context(), &diffC1, &diffC2, &aC1, &aC2, &bC1, &bC2) != 1)
return tecINTERNAL;
if (!serializeEcPair(diffC1, diffC2, out))
return tecINTERNAL;
return tesSUCCESS;
}
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)
{
if (proof.length() != ecEqualityProofLength)
return tecINTERNAL;
secp256k1_pubkey c1;
secp256k1_pubkey c2;
if (!makeEcPair(encAmt, c1, c2))
return tecINTERNAL;
// todo: might need to change how its hashed
Serializer s;
s.addRaw(txHash.data(), txHash.bytes);
s.add32(spendVersion);
// auto const txContextId = s.getSHA512Half();
// todo: support equality
// if (secp256k1_equality_verify(
// secp256k1Context(),
// reinterpret_cast<unsigned char const*>(proof.data()),
// proof.length(), // Length of the proof byte array (98 bytes)
// &c1,
// &c2,
// reinterpret_cast<unsigned char const*>(pubkey.data()),
// amount,
// txContextId.data(), // Transaction context data
// txContextId.bytes // Length of context data
// ) != 1)
// return tecBAD_PROOF;
return tesSUCCESS;
}
Buffer
encryptAmount(uint64_t amt, Slice const& pubKeySlice)
{
Buffer buf(ecGamalEncryptedTotalLength);
// Allocate ciphertext placeholders
secp256k1_pubkey c1, c2;
// todo: might need to be updated using another RNG
// Prepare a random blinding factor
unsigned char blindingFactor[32];
if (RAND_bytes(blindingFactor, 32) != 1)
Throw<std::runtime_error>("Failed to generate random number");
secp256k1_pubkey pubKey;
std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength);
// Encrypt the amount
if (!secp256k1_elgamal_encrypt(
secp256k1Context(), &c1, &c2, &pubKey, amt, blindingFactor))
Throw<std::runtime_error>("Failed to encrypt amount");
// Serialize the ciphertext pair into the buffer
if (!serializeEcPair(c1, c2, buf))
Throw<std::runtime_error>(
"Failed to serialize into 66 byte compressed format");
return buf;
}
Buffer
encryptCanonicalZeroAmount(
Slice const& pubKeySlice,
AccountID const& account,
MPTID const& mptId)
{
Buffer buf(ecGamalEncryptedTotalLength);
// Allocate ciphertext placeholders
secp256k1_pubkey c1, c2;
secp256k1_pubkey pubKey;
std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength);
// Encrypt the amount
if (!generate_canonical_encrypted_zero(
secp256k1Context(),
&c1,
&c2,
&pubKey,
account.data(),
mptId.data()))
Throw<std::runtime_error>("Failed to encrypt amount");
// Serialize the ciphertext pair into the buffer
if (!serializeEcPair(c1, c2, buf))
Throw<std::runtime_error>(
"Failed to serialize into 66 byte compressed format");
return buf;
}
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)
{
// if (proof.length() != ecConfidentialSendProofLength)
// return tecINTERNAL;
secp256k1_pubkey balC1, balC2;
if (!makeEcPair(encSenderBalance, balC1, balC2))
return tecINTERNAL;
secp256k1_pubkey senderC1, senderC2;
if (!makeEcPair(encSenderAmt, senderC1, senderC2))
return tecINTERNAL;
secp256k1_pubkey destC1, destC2;
if (!makeEcPair(encDestAmt, destC1, destC2))
return tecINTERNAL;
secp256k1_pubkey issuerC1, issuerC2;
if (!makeEcPair(encIssuerAmt, issuerC1, issuerC2))
return tecINTERNAL;
Serializer s;
s.addRaw(txHash.data(), txHash.bytes);
s.add32(version);
// auto const txContextId = s.getSHA512Half();
// todo: equality and range proof verification
// if (secp256k1_equal_range_verify(
// secp256k1Context(),
// reinterpret_cast<unsigned char const*>(proof.data()),
// proof.length(),
// txContextId.data(),
// &balC1,
// &balC2,
// &senderC1,
// &senderC2,
// reinterpret_cast<unsigned char const*>(senderPubKey.data()),
// &destC1,
// &destC2,
// reinterpret_cast<unsigned char const*>(destPubKey.data()),
// &issuerC1,
// &issuerC2,
// reinterpret_cast<unsigned char const*>(issuerPubKey.data()),
// txContextId.data(),
// txContextId.bytes) != 1)
// return tecBAD_PROOF;
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -127,6 +127,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."),
@@ -219,6 +220,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

@@ -19,13 +19,34 @@
#include <test/jtx.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/jss.h>
#include "test/jtx/mpt.h"
#include <openssl/rand.h>
#include <cstdint>
#include <string>
namespace ripple {
namespace test {
namespace jtx {
ripple::Buffer
generatePlaceholderCiphertext()
{
Buffer buf(ecGamalEncryptedTotalLength);
buf.data()[0] = 0x02;
buf.data()[ecGamalEncryptedLength] = 0x02;
buf.data()[ecGamalEncryptedLength - 1] = 0x01;
buf.data()[ecGamalEncryptedTotalLength - 1] = 0x01;
return buf;
}
void
mptflags::operator()(Env& env) const
{
@@ -248,60 +269,79 @@ MPTTester::set(MPTSet const& arg)
jv[sfTransferFee] = *arg.transferFee;
if (arg.metadata)
jv[sfMPTokenMetadata] = strHex(*arg.metadata);
if (submit(arg, jv) == tesSUCCESS && (arg.flags || arg.mutableFlags))
if (arg.pubKey)
jv[sfIssuerElGamalPublicKey] = strHex(*arg.pubKey);
if (submit(arg, jv) == tesSUCCESS)
{
auto require = [&](std::optional<Account> const& holder,
bool unchanged) {
auto flags = getFlags(holder);
if (!unchanged)
{
if (arg.flags)
if ((arg.flags || arg.mutableFlags))
{
auto require = [&](std::optional<Account> const& holder,
bool unchanged) {
auto flags = getFlags(holder);
if (!unchanged)
{
if (*arg.flags & tfMPTLock)
flags |= lsfMPTLocked;
else if (*arg.flags & tfMPTUnlock)
flags &= ~lsfMPTLocked;
if (arg.flags)
{
if (*arg.flags & tfMPTLock)
flags |= lsfMPTLocked;
else if (*arg.flags & tfMPTUnlock)
flags &= ~lsfMPTLocked;
}
if (arg.mutableFlags)
{
if (*arg.mutableFlags & tmfMPTSetCanLock)
flags |= lsfMPTCanLock;
else if (*arg.mutableFlags & tmfMPTClearCanLock)
flags &= ~lsfMPTCanLock;
if (*arg.mutableFlags & tmfMPTSetRequireAuth)
flags |= lsfMPTRequireAuth;
else if (*arg.mutableFlags & tmfMPTClearRequireAuth)
flags &= ~lsfMPTRequireAuth;
if (*arg.mutableFlags & tmfMPTSetCanEscrow)
flags |= lsfMPTCanEscrow;
else if (*arg.mutableFlags & tmfMPTClearCanEscrow)
flags &= ~lsfMPTCanEscrow;
if (*arg.mutableFlags & tmfMPTSetCanClawback)
flags |= lsfMPTCanClawback;
else if (*arg.mutableFlags & tmfMPTClearCanClawback)
flags &= ~lsfMPTCanClawback;
if (*arg.mutableFlags & tmfMPTSetCanTrade)
flags |= lsfMPTCanTrade;
else if (*arg.mutableFlags & tmfMPTClearCanTrade)
flags &= ~lsfMPTCanTrade;
if (*arg.mutableFlags & tmfMPTSetCanTransfer)
flags |= lsfMPTCanTransfer;
else if (*arg.mutableFlags & tmfMPTClearCanTransfer)
flags &= ~lsfMPTCanTransfer;
}
}
env_.require(mptflags(*this, flags, holder));
};
if (arg.account)
require(std::nullopt, arg.holder.has_value());
if (arg.holder)
require(*arg.holder, false);
}
if (arg.mutableFlags)
{
if (*arg.mutableFlags & tmfMPTSetCanLock)
flags |= lsfMPTCanLock;
else if (*arg.mutableFlags & tmfMPTClearCanLock)
flags &= ~lsfMPTCanLock;
if (*arg.mutableFlags & tmfMPTSetRequireAuth)
flags |= lsfMPTRequireAuth;
else if (*arg.mutableFlags & tmfMPTClearRequireAuth)
flags &= ~lsfMPTRequireAuth;
if (*arg.mutableFlags & tmfMPTSetCanEscrow)
flags |= lsfMPTCanEscrow;
else if (*arg.mutableFlags & tmfMPTClearCanEscrow)
flags &= ~lsfMPTCanEscrow;
if (*arg.mutableFlags & tmfMPTSetCanClawback)
flags |= lsfMPTCanClawback;
else if (*arg.mutableFlags & tmfMPTClearCanClawback)
flags &= ~lsfMPTCanClawback;
if (*arg.mutableFlags & tmfMPTSetCanTrade)
flags |= lsfMPTCanTrade;
else if (*arg.mutableFlags & tmfMPTClearCanTrade)
flags &= ~lsfMPTCanTrade;
if (*arg.mutableFlags & tmfMPTSetCanTransfer)
flags |= lsfMPTCanTransfer;
else if (*arg.mutableFlags & tmfMPTClearCanTransfer)
flags &= ~lsfMPTCanTransfer;
}
}
env_.require(mptflags(*this, flags, holder));
};
if (arg.account)
require(std::nullopt, arg.holder.has_value());
if (arg.holder)
require(*arg.holder, false);
if (arg.pubKey)
{
env_.require(requireAny([&]() -> bool {
return forObject([&](SLEP const& sle) -> bool {
if (sle)
{
return strHex((*sle)[sfIssuerElGamalPublicKey]) ==
strHex(getPubKey(issuer_));
}
return false;
});
}));
}
}
}
@@ -329,6 +369,17 @@ MPTTester::checkDomainID(std::optional<uint256> expected) const
});
}
[[nodiscard]] bool
MPTTester::printMPT(Account const& holder_) const
{
return forObject(
[&](SLEP const& sle) -> bool {
std::cout << "\n" << sle->getJson();
return true;
},
holder_);
}
[[nodiscard]] bool
MPTTester::checkMPTokenAmount(
Account const& holder_,
@@ -347,6 +398,15 @@ MPTTester::checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const
});
}
[[nodiscard]] bool
MPTTester::checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const
{
return forObject([&](SLEP const& sle) {
return expectedAmount ==
(*sle)[~sfConfidentialOutstandingAmount].value_or(0);
});
}
[[nodiscard]] bool
MPTTester::checkFlags(
uint32_t const expectedFlags,
@@ -492,6 +552,48 @@ MPTTester::getBalance(Account const& account) const
return 0;
}
std::int64_t
MPTTester::getIssuanceConfidentialBalance() const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
if (auto const sle = env_.le(keylet::mptIssuance(*id_)))
return (*sle)[~sfConfidentialOutstandingAmount].value_or(0);
return 0;
}
std::optional<Buffer>
MPTTester::getEncryptedBalance(
Account const& account,
EncryptedBalanceType option) const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
if (auto const sle = env_.le(keylet::mptoken(*id_, account.id())))
{
if (option == HOLDER_ENCRYPTED_INBOX &&
sle->isFieldPresent(sfConfidentialBalanceInbox))
return Buffer(
(*sle)[sfConfidentialBalanceInbox].data(),
(*sle)[sfConfidentialBalanceInbox].size());
if (option == HOLDER_ENCRYPTED_SPENDING &&
sle->isFieldPresent(sfConfidentialBalanceSpending))
return Buffer(
(*sle)[sfConfidentialBalanceSpending].data(),
(*sle)[sfConfidentialBalanceSpending].size());
if (option == ISSUER_ENCRYPTED_BALANCE &&
sle->isFieldPresent(sfIssuerEncryptedBalance))
return Buffer(
(*sle)[sfIssuerEncryptedBalance].data(),
(*sle)[sfIssuerEncryptedBalance].size());
}
return {};
}
std::uint32_t
MPTTester::getFlags(std::optional<Account> const& holder) const
{
@@ -512,6 +614,533 @@ MPTTester::operator[](std::string const& name) const
return MPT(name, issuanceID());
}
void
MPTTester::convert(MPTConvert const& arg)
{
Json::Value jv;
if (arg.account)
jv[sfAccount] = arg.account->human();
else
Throw<std::runtime_error>("Account not specified");
jv[jss::TransactionType] = jss::ConfidentialConvert;
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
jv[sfMPTokenIssuanceID] = to_string(*id_);
}
if (arg.amt)
jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
if (arg.holderPubKey)
jv[sfHolderElGamalPublicKey.jsonName] = strHex(*arg.holderPubKey);
if (arg.holderEncryptedAmt)
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
else
jv[sfHolderEncryptedAmount.jsonName] =
strHex(encryptAmount(*arg.account, *arg.amt));
if (arg.issuerEncryptedAmt)
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
else
jv[sfIssuerEncryptedAmount.jsonName] =
strHex(encryptAmount(issuer_, *arg.amt));
if (arg.proof)
jv[sfZKProof.jsonName] = *arg.proof;
auto const holderAmt = getBalance(*arg.account);
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
uint64_t prevInboxBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t prevSpendingBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
uint64_t prevIssuerBalance =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
if (submit(arg, jv) == tesSUCCESS)
{
auto const postConfidentialOutstanding =
getIssuanceConfidentialBalance();
env_.require(mptbalance(*this, *arg.account, holderAmt - *arg.amt));
env_.require(requireAny([&]() -> bool {
return prevConfidentialOutstanding + *arg.amt ==
postConfidentialOutstanding;
}));
uint64_t postInboxBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t postIssuerBalance =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
uint64_t postSpendingBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
// spending balance should not change
env_.require(requireAny([&]() -> bool {
return postSpendingBalance == prevSpendingBalance;
}));
// issuer's encrypted balance is updated correctly
env_.require(requireAny([&]() -> bool {
return prevIssuerBalance + *arg.amt == postIssuerBalance;
}));
// holder's inbox balance is updated correctly
env_.require(requireAny([&]() -> bool {
return prevInboxBalance + *arg.amt == postInboxBalance;
}));
// sum of holder's inbox and spending balance should equal to issuer's
// encrypted balance
env_.require(requireAny([&]() -> bool {
return postInboxBalance + postSpendingBalance == postIssuerBalance;
}));
if (arg.holderPubKey)
{
env_.require(requireAny([&]() -> bool {
return forObject(
[&](SLEP const& sle) -> bool {
if (sle)
{
return strHex((*sle)[sfHolderElGamalPublicKey]) ==
strHex(getPubKey(*arg.account));
}
return false;
},
*arg.account);
}));
}
}
}
void
MPTTester::send(MPTConfidentialSend const& arg)
{
Json::Value jv;
if (arg.account)
jv[sfAccount] = arg.account->human();
else
Throw<std::runtime_error>("Account not specified");
if (arg.dest)
jv[sfDestination] = arg.dest->human();
else
Throw<std::runtime_error>("Destination not specified");
jv[jss::TransactionType] = jss::ConfidentialSend;
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
jv[sfMPTokenIssuanceID] = to_string(*id_);
}
// Generate the encrypted amounts if not provided
if (arg.senderEncryptedAmt)
jv[sfSenderEncryptedAmount] = strHex(*arg.senderEncryptedAmt);
else
jv[sfSenderEncryptedAmount] =
strHex(encryptAmount(*arg.account, *arg.amt));
if (arg.destEncryptedAmt)
jv[sfDestinationEncryptedAmount] = strHex(*arg.destEncryptedAmt);
else
jv[sfDestinationEncryptedAmount] =
strHex(encryptAmount(*arg.dest, *arg.amt));
if (arg.issuerEncryptedAmt)
jv[sfIssuerEncryptedAmount] = strHex(*arg.issuerEncryptedAmt);
else
jv[sfIssuerEncryptedAmount] = strHex(encryptAmount(issuer_, *arg.amt));
if (arg.proof)
jv[sfZKProof] = *arg.proof;
if (arg.credentials)
{
auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue);
for (auto const& hash : *arg.credentials)
arr.append(hash);
}
auto const senderPubAmt = getBalance(*arg.account);
auto const destPubAmt = getBalance(*arg.dest);
auto const prevCOA = getIssuanceConfidentialBalance();
auto const prevOA = getIssuanceOutstandingBalance();
// Sender's previous confidential state
uint64_t prevSenderInbox =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t prevSenderSpending =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
uint64_t prevSenderIssuer =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
// Destination's previous confidential state
uint64_t prevDestInbox =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
uint64_t prevDestSpending =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
uint64_t prevDestIssuer =
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
auto const postOA = getIssuanceOutstandingBalance();
// Sender's post confidential state
uint64_t postSenderInbox =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t postSenderSpending =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
uint64_t postSenderIssuer =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
// Destination's post confidential state
uint64_t postDestInbox =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
uint64_t postDestSpending =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
uint64_t postDestIssuer =
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
// Public balances unchanged
env_.require(mptbalance(*this, *arg.account, senderPubAmt));
env_.require(mptbalance(*this, *arg.dest, destPubAmt));
// OA and COA unchanged
env_.require(requireAny([&]() -> bool { return prevOA == postOA; }));
env_.require(requireAny([&]() -> bool { return prevCOA == postCOA; }));
// Verify sender changes
env_.require(requireAny([&]() -> bool {
return prevSenderSpending >= *arg.amt &&
postSenderSpending == prevSenderSpending - *arg.amt;
}));
env_.require(requireAny(
[&]() -> bool { return postSenderInbox == prevSenderInbox; }));
env_.require(requireAny([&]() -> bool {
return prevSenderIssuer >= *arg.amt &&
postSenderIssuer == prevSenderIssuer - *arg.amt;
}));
// Verify destination changes
env_.require(requireAny([&]() -> bool {
return postDestInbox == prevDestInbox + *arg.amt;
}));
env_.require(requireAny(
[&]() -> bool { return postDestSpending == prevDestSpending; }));
env_.require(requireAny([&]() -> bool {
return postDestIssuer == prevDestIssuer + *arg.amt;
}));
// Cross checks
env_.require(requireAny([&]() -> bool {
return postSenderInbox + postSenderSpending == postSenderIssuer;
}));
env_.require(requireAny([&]() -> bool {
return postDestInbox + postDestSpending == postDestIssuer;
}));
}
}
void
MPTTester::confidentialClaw(MPTConfidentialClawback const& arg)
{
Json::Value jv;
auto const account = arg.account ? *arg.account : issuer_;
jv[sfAccount] = account.human();
if (arg.holder)
jv[sfHolder] = arg.holder->human();
else
Throw<std::runtime_error>("Holder not specified");
jv[jss::TransactionType] = jss::ConfidentialClawback;
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else if (id_)
jv[sfMPTokenIssuanceID] = to_string(*id_);
else
Throw<std::runtime_error>("MPT has not been created");
if (arg.amt)
jv[sfMPTAmount] = std::to_string(*arg.amt);
if (arg.proof)
jv[sfZKProof] = *arg.proof;
auto const holderPubAmt = getBalance(*arg.holder);
auto const prevCOA = getIssuanceConfidentialBalance();
auto const prevOA = getIssuanceOutstandingBalance();
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
auto const postOA = getIssuanceOutstandingBalance();
// Verify holder's public balance is unchanged
env_.require(mptbalance(*this, *arg.holder, holderPubAmt));
// Verify COA and OA are reduced correctly
env_.require(requireAny([&]() -> bool {
return prevCOA >= *arg.amt && postCOA == prevCOA - *arg.amt;
}));
env_.require(requireAny([&]() -> bool {
return prevOA >= *arg.amt && postOA == prevOA - *arg.amt;
}));
// Verify holder's confidential balances are zeroed out
env_.require(requireAny([&]() -> bool {
return getDecryptedBalance(*arg.holder, HOLDER_ENCRYPTED_INBOX) ==
0;
}));
env_.require(requireAny([&]() -> bool {
return getDecryptedBalance(
*arg.holder, HOLDER_ENCRYPTED_SPENDING) == 0;
}));
env_.require(requireAny([&]() -> bool {
return getDecryptedBalance(*arg.holder, ISSUER_ENCRYPTED_BALANCE) ==
0;
}));
}
}
void
MPTTester::generateKeyPair(Account const& account)
{
unsigned char privKey[ecPrivKeyLength];
secp256k1_pubkey pubKey;
if (!secp256k1_elgamal_generate_keypair(
secp256k1Context(), privKey, &pubKey))
Throw<std::runtime_error>("failed to generate key pair");
pubKeys.insert({account.id(), Buffer{pubKey.data, ecPubKeyLength}});
privKeys.insert({account.id(), Buffer{privKey, ecPrivKeyLength}});
}
Buffer
MPTTester::getPubKey(Account const& account) const
{
auto it = pubKeys.find(account.id());
if (it != pubKeys.end())
{
return it->second;
}
Throw<std::runtime_error>("Account does not have public key");
}
Buffer
MPTTester::getPrivKey(Account const& account) const
{
auto it = privKeys.find(account.id());
if (it != privKeys.end())
{
return it->second;
}
Throw<std::runtime_error>("Account does not have private key");
}
Buffer
MPTTester::encryptAmount(Account const& account, uint64_t amt) const
{
return ripple::encryptAmount(amt, getPubKey(account));
}
uint64_t
MPTTester::decryptAmount(Account const& account, Buffer const& amt) const
{
secp256k1_pubkey c1;
secp256k1_pubkey c2;
uint64_t decryptedAmt;
if (!makeEcPair(amt, c1, c2))
Throw<std::runtime_error>(
"Failed to convert into individual EC components");
if (!secp256k1_elgamal_decrypt(
secp256k1Context(),
&decryptedAmt,
&c1,
&c2,
getPrivKey(account).data()))
Throw<std::runtime_error>("Failed to decrypt amount");
return decryptedAmt;
}
uint64_t
MPTTester::getDecryptedBalance(
Account const& account,
EncryptedBalanceType balanceType) const
{
auto maybeEncrypted = getEncryptedBalance(account, balanceType);
auto accountToDecrypt =
balanceType == ISSUER_ENCRYPTED_BALANCE ? issuer_ : account;
return maybeEncrypted ? decryptAmount(accountToDecrypt, *maybeEncrypted)
: 0;
};
void
MPTTester::mergeInbox(MPTMergeInbox const& arg)
{
Json::Value jv;
if (arg.account)
jv[sfAccount] = arg.account->human();
else
Throw<std::runtime_error>("Account not specified");
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
jv[sfMPTokenIssuanceID] = to_string(*id_);
}
jv[sfTransactionType] = jss::ConfidentialMergeInbox;
uint64_t prevInboxBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t prevSpendingBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
uint64_t prevIssuerBalance =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
if (submit(arg, jv) == tesSUCCESS)
{
uint64_t postInboxBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t postSpendingBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
uint64_t postIssuerBalance =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
env_.require(requireAny([&]() -> bool {
return postSpendingBalance ==
prevInboxBalance + prevSpendingBalance &&
postInboxBalance == 0;
}));
env_.require(requireAny(
[&]() -> bool { return prevIssuerBalance == postIssuerBalance; }));
env_.require(requireAny([&]() -> bool {
return postSpendingBalance + postInboxBalance == postIssuerBalance;
}));
}
}
std::int64_t
MPTTester::getIssuanceOutstandingBalance() const
{
if (!id_)
Throw<std::runtime_error>("Issuance ID does not exist");
auto const sle = env_.current()->read(keylet::mptIssuance(*id_));
if (!sle || !sle->isFieldPresent(sfOutstandingAmount))
Throw<std::runtime_error>(
"Issuance object does not contain outstanding amount");
return (*sle)[sfOutstandingAmount];
}
void
MPTTester::convertBack(MPTConvertBack const& arg)
{
Json::Value jv;
if (arg.account)
jv[sfAccount] = arg.account->human();
else
Throw<std::runtime_error>("Account not specified");
jv[jss::TransactionType] = jss::ConfidentialConvertBack;
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
jv[sfMPTokenIssuanceID] = to_string(*id_);
}
if (arg.amt)
jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
if (arg.holderEncryptedAmt)
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
else
jv[sfHolderEncryptedAmount.jsonName] =
strHex(encryptAmount(*arg.account, *arg.amt));
if (arg.issuerEncryptedAmt)
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
else
jv[sfIssuerEncryptedAmount.jsonName] =
strHex(encryptAmount(issuer_, *arg.amt));
if (arg.proof)
jv[sfZKProof.jsonName] = *arg.proof;
auto const holderAmt = getBalance(*arg.account);
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
uint64_t prevInboxBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t prevSpendingBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
uint64_t prevIssuerBalance =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
if (submit(arg, jv) == tesSUCCESS)
{
auto const postConfidentialOutstanding =
getIssuanceConfidentialBalance();
env_.require(mptbalance(*this, *arg.account, holderAmt + *arg.amt));
env_.require(requireAny([&]() -> bool {
return prevConfidentialOutstanding - *arg.amt ==
postConfidentialOutstanding;
}));
uint64_t postInboxBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t postIssuerBalance =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
uint64_t postSpendingBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
// inbox balance should not change
env_.require(requireAny(
[&]() -> bool { return postInboxBalance == prevInboxBalance; }));
// issuer's encrypted balance is updated correctly
env_.require(requireAny([&]() -> bool {
return prevIssuerBalance - *arg.amt == postIssuerBalance;
}));
// holder's spending balance is updated correctly
env_.require(requireAny([&]() -> bool {
return prevSpendingBalance - *arg.amt == postSpendingBalance;
}));
// sum of holder's inbox and spending balance should equal to issuer's
// encrypted balance
env_.require(requireAny([&]() -> bool {
return postInboxBalance + postSpendingBalance == postIssuerBalance;
}));
}
}
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -27,12 +27,18 @@
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
namespace ripple {
namespace test {
namespace jtx {
class MPTTester;
// Generates a syntactically valid placeholder ciphertext
ripple::Buffer
generatePlaceholderCiphertext();
// Check flags settings on MPT create
class mptflags
{
@@ -145,6 +151,77 @@ 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<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;
};
@@ -155,8 +232,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 = {});
void
@@ -171,6 +256,22 @@ public:
void
set(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;
@@ -181,6 +282,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,
@@ -234,9 +338,43 @@ 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;
bool
printMPT(Account const& holder_) const;
void
generateKeyPair(Account const& account);
Buffer
getPubKey(Account const& account) const;
Buffer
getPrivKey(Account const& account) const;
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;
private:
using SLEP = std::shared_ptr<SLE const>;
bool

View File

@@ -0,0 +1,170 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#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
if (ctx.tx[sfMPTAmount] >
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0))
return tecINSUFFICIENT_FUNDS;
// todo: ZKP Verification
// verify the MPT amount to clawback is the holder's confidential balance
// if (!isTesSuccess(terProof))
// return tecBAD_PROOF;
return tesSUCCESS;
}
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,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_CONFIDENTIALCLAWSBACK_H_INCLUDED
#define RIPPLE_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,235 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#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;
// if (ctx.tx[sfZKProof].length() != 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(lsfMPTNoConfidentialTransfer))
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];
// todo: check zkproof/well formed
// check equality proof
// 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], holderPubKey)) ||
// !isTesSuccess(checkEqualityProof(
// ctx.tx[sfIssuerEncryptedAmount],
// (*sleIssuance)[sfIssuerElGamalPublicKey])))
// {
// 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]);
(*sleMptoken)[sfConfidentialBalanceSpending] = out;
}
catch (std::exception const& e)
{
return tecINTERNAL;
}
}
else
{
// both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
// exist together
return tecINTERNAL;
}
view().update(sleIssuance);
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_CONFIDENTIALCONVERT_H_INCLUDED
#define RIPPLE_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,197 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#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(lsfMPTNoConfidentialTransfer))
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,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_CONFIDENTIALCONVERTBACK_H_INCLUDED
#define RIPPLE_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,111 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#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(lsfMPTNoConfidentialTransfer))
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,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_CONFIDENTIALMERGEINBOX_H_INCLUDED
#define RIPPLE_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,257 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#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(lsfMPTNoConfidentialTransfer))
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,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_CONFIDENTIALSEND_H_INCLUDED
#define RIPPLE_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

@@ -93,6 +93,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

@@ -37,6 +37,10 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
!ctx.rules.enabled(featureDynamicMPT))
return false;
if (ctx.tx.getFlags() & tfMPTNoConfidentialTransfer &&
!ctx.rules.enabled(featureConfidentialTransfer))
return false;
return true;
}

View File

@@ -78,6 +78,18 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
return temMALFORMED;
if (!ctx.rules.enabled(featureConfidentialTransfer) &&
ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey))
return temDISABLED;
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) &&
ctx.tx.isFieldPresent(sfHolder))
return temMALFORMED;
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) &&
ctx.tx[sfIssuerElGamalPublicKey].length() != ecPubKeyLength)
return temMALFORMED;
auto const txFlags = ctx.tx.getFlags();
// fails if both flags are set
@@ -90,10 +102,12 @@ 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 && !ctx.tx.isFieldPresent(sfDomainID) &&
!ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && !isMutate)
return temMALFORMED;
}
@@ -264,6 +278,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(tfMPTNoConfidentialTransfer))
{
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
@@ -351,6 +378,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;