Compare commits

...

74 Commits

Author SHA1 Message Date
Shawn Xie
3aa84194ac Merge branch 'confidential-transfer' into ripple/confidential-devnet 2026-02-26 13:25:34 -05:00
Peter Chen
6b56fc2644 chore: Bring confidential-devnet to tip of confidential/transfer branch (#6435) 2026-02-26 13:03:04 -05:00
Peter Chen
c2f8b91397 Add invariants and tests (#6403) 2026-02-25 16:40:44 -05:00
Peter Chen
1ea9312946 update crypto-lib (#6418) 2026-02-25 12:18:31 -05:00
yinyiqian1
6ad60d7141 Support Range Proof for ConfidentialMPTSend (#6404)
- proving send amount m is in the range [0, 2^64)
- proving remaining balance b-m is in the range [0, 2^64)
2026-02-20 14:18:34 -05:00
Shawn Xie
94e911ed69 Add Range Proof Verification to ConvertBack (#6377) 2026-02-19 19:22:48 -05:00
Shawn Xie
b2c434dd73 Compress ElGamal Public Keys and Pedersen Commitments + Add Validation (#6385) 2026-02-19 08:41:15 -05:00
Peter Chen
b6d1a8d62b Test Auditor for Confidential Send and revealed R (#6320) 2026-02-17 14:27:33 -05:00
Shawn Xie
9d0c854139 Remove hardcoded library functions in ConfidentialTransfer (#6365) 2026-02-12 14:18:36 -05:00
Ayaz Salikhov
3a6ca681ff chore: Use mpt-crypto library (#6362) 2026-02-12 12:59:36 -05:00
Shawn Xie
a216824c15 Merge develop into confidential-transfer 2026-02-12 11:40:07 -05:00
Shawn Xie
90cf86a920 remove newline 2026-02-12 11:33:34 -05:00
Shawn Xie
e69d3c9bd7 Merge remote-tracking branch 'upstream/develop' into ct-merge-develop-lib 2026-02-12 11:14:40 -05:00
Shawn Xie
fd390a4f1c Add doxygen comments for new transactions and helper functions (#6332) 2026-02-10 10:51:50 -05:00
Shawn Xie
3941283438 Prefix confidential transfer transaction names with "MPT" (#6312) 2026-02-02 12:13:18 -05:00
Shawn Xie
86af28d91d Apply clang-format due to new column size (#6311) 2026-02-02 11:15:39 -05:00
Shawn Xie
41f7102fb8 Merge develop into ripple/confidential-transfer
Merge `develop` into `ripple/confidential-transfer`
2026-02-02 09:46:25 -05:00
Shawn Xie
66ed0fa452 namespace rename 2026-01-30 12:36:15 -05:00
Shawn Xie
cad8fb328a Merge branch 'develop' into ct-merge-develop-new 2026-01-30 12:25:55 -05:00
Shawn Xie
31346425f0 Merge commit '5f638f55536def0d88b970d1018a465a238e55f4' into ct-merge-develop-new 2026-01-30 12:25:36 -05:00
Shawn Xie
40bfaa25d2 Merge commit '92046785d1fea5f9efe5a770d636792ea6cab78b' into ct-merge-develop-new 2026-01-30 12:25:16 -05:00
Peter Chen
c4916f1251 Add more Auditor Tests for Convert and ConvertBack (#6255) 2026-01-29 12:17:19 -05:00
yinyiqian1
fc8b7898c5 Support Pedersen-ElGamal linkage for ConfidentialSend (#6289)
* support Pedersen Amount commitment for ConfidentialSend
* support Pedersen Balance commitment for ConfidentialSend
2026-01-29 11:18:46 -05:00
Shawn Xie
446f9fbe6d Reuse getConfidentialRecipientCount (#6281) 2026-01-26 13:04:02 -05:00
yinyiqian1
1297385b7e Support ConfidentialSend equality proof (#6274)
* Support ConfidentialSend equality proof

* resolve conflicts

* Add version check in send
2026-01-26 12:39:35 -05:00
Shawn Xie
114adc0c57 Pedersen commitment with ConvertBack and basic test (#6243) 2026-01-22 13:00:19 -05:00
yinyiqian1
1d349c32c5 fix encrypt zero balance and remove improper throw (#6242) 2026-01-20 12:27:44 -05:00
Shawn Xie
a5f20c129d Copying over pedersen commitment from crypto lib (#6238) 2026-01-19 13:56:10 -05:00
yinyiqian1
75d143a2a0 support new design to reveal blinding factor (#6237)
* reveal blinding factor and optimize
* schnorr proof is added for registering holder pub key
* clean env.close that already closed
* clean up the lib functions
2026-01-19 13:07:19 -05:00
Shawn Xie
e3da98e310 Update unit test framework to use shared random factor (#6233) 2026-01-16 16:36:49 -05:00
Shawn Xie
ec6d7cb91d Add equality proof to ConvertBack and refactor to reduce redundancy (#6220) 2026-01-16 10:28:55 -05:00
Shawn Xie
fa055c2bd5 Add auditing feature across confidential transfer transactions (#6200) 2026-01-14 11:18:06 -05:00
Shawn Xie
6c38086f17 ConfidentialConvert with Equality Proof (#6177) 2026-01-07 16:17:07 -05:00
Shawn Xie
3e9dc276ed add back clawback hash (#6175) 2026-01-06 12:21:00 -05:00
Shawn Xie
abf7a62b1f Refactor proof (#6168) 2026-01-05 12:00:41 -05:00
yinyiqian1
bd3a6e1631 Support equality proof for confidential clawback (#6149) 2026-01-02 11:48:06 -05:00
yinyiqian1
7c0bd419a4 support mutability for MPTPrivacy (#6137)
Update lsfMPTNoConfidentialTransfer to lsfMPTPrivacy
Add flag lsmfMPTPrivacy to control the mutability of lsfMPTPrivacy.
disallow mutating lsfMPTPrivacy when lsfMPTPrivacy is not set.
disallow mutating lsfMPTPrivacy when there's confidential outstanding amount.
2025-12-10 17:10:33 -05:00
yinyiqian1
d3126959e7 Merge pull request #6123 from yinyiqian1/merge
Merge remote-tracking branch 'origin/develop' into 'ripple/confidential-transfer'
2025-12-09 10:49:11 -05:00
yinyiqian1
67e8e89e0f copyright fix 2025-12-08 18:36:34 -05:00
yinyiqian1
4e4326a174 trigger ci 2025-12-08 18:25:47 -05:00
yinyiqian1
5397bd6d6e fix naming 2025-12-08 17:58:32 -05:00
yinyiqian1
6dece25cc3 fix test failure 2025-12-08 17:34:20 -05:00
yinyiqian1
d9da8733be resolve pre-commit clang-format 2025-12-08 16:45:41 -05:00
yinyiqian1
f6f51451e7 Merge remote-tracking branch 'origin/develop' into merge 2025-12-08 15:09:04 -05:00
Shawn Xie
3c8ec2eb7e fix to lower case 2025-11-24 09:55:58 -05:00
Shawn Xie
c754aa3bca enable confidential transfer 2025-11-24 09:54:56 -05:00
Shawn Xie
b94c95b3e9 Change err code (#6050) 2025-11-18 14:19:41 -05:00
yinyiqian1
8365148b5c feat: support ConfidentialClawback and add tests (#6023) 2025-11-13 14:24:40 -05:00
Shawn Xie
c03866bf0f Variable rename (#6028) 2025-11-12 11:58:05 -05:00
Shawn Xie
389afc5f06 Add deposit preauth and other checks (#6011) 2025-11-10 10:52:23 -05:00
Shawn Xie
7b04eaae81 ConvertBack preclaim tests (#6006) 2025-11-05 13:58:52 -05:00
Shawn Xie
1343019509 ConvertBack tests (#6005) 2025-11-05 13:52:55 -05:00
Shawn Xie
cd75e630a2 Change ConfidentialSend preflight error code (#5994) 2025-11-03 18:46:27 -05:00
Shawn Xie
ec57fbdc5f Merge remote-tracking branch 'upstream/develop' into confidential-transfer 2025-11-03 18:42:41 -05:00
Shawn Xie
4fe67f5715 ConvertBack preflight tests (#5991) 2025-11-03 15:58:32 -05:00
Shawn Xie
44d885e39b Basic ConvertBack test (#5979) 2025-10-31 11:46:24 -04:00
yinyiqian1
3af758145c Check auth for ConfidentialSend (#5968) 2025-10-30 11:02:46 -04:00
yinyiqian1
f3d4d4341b add ciphertext check for ConfidentialSend (#5964) 2025-10-29 12:10:48 -04:00
Shawn Xie
ddb518ad09 MergeInbox tests (#5949) 2025-10-28 13:21:11 -04:00
Shawn Xie
3899e3f36c Add auth checks for convert (#5937) 2025-10-24 11:42:43 -04:00
yinyiqian1
e4a8ba51f9 check lock in ConfidentialSend (#5933) 2025-10-23 12:58:38 -04:00
Shawn Xie
35e4fad557 Add ciphertext check (#5930) 2025-10-23 11:57:18 -04:00
yinyiqian1
8e9cb3c1da support ConfidentialSend (#5921) 2025-10-22 12:02:00 -04:00
Shawn Xie
18d92058e3 MergeInbox (#5922) 2025-10-22 11:30:44 -04:00
Shawn Xie
f24d584f29 ConfidentialConvert tests (#5911) 2025-10-20 14:39:16 -04:00
Shawn Xie
da3fbcd25b Remove unused header file (#5908) 2025-10-17 16:42:08 -04:00
Shawn Xie
daa1303b5a Update decryption test helper function (#5907) 2025-10-17 14:19:19 -04:00
Shawn Xie
a636fe5871 Update test framework for encryption (#5906) 2025-10-17 14:04:54 -04:00
Shawn Xie
bbc3071fd1 Update mpt-crypto with zero encryption (#5905) 2025-10-17 11:41:39 -04:00
Shawn Xie
8fdc639206 ConfidentialConvert (#5901)
ConfidentialConvert and some test framework update
2025-10-16 14:31:14 -04:00
Shawn Xie
5a89641d98 remove duplicate code 2025-10-07 15:52:18 -04:00
Shawn Xie
beefa248a6 Merge remote-tracking branch 'upstream/develop' into confidential-transfer 2025-10-07 15:00:14 -04:00
Shawn Xie
e919a25ecb Merge develop into ripple/confidential-transfer (#5835)
* Fix: Don't flag consensus as stalled prematurely (#5658)

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

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

* Set version to 2.5.1

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

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

* Fix: EscrowTokenV1 (#5571)

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

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

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

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

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

* Support DynamicMPT XLS-94d (#5705)

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

* Bugfix: Adds graceful peer disconnection (#5669)

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

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

* Downgrade to boost 1.83

* Set version to 2.6.1-rc1

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

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

* Rename mutable flags (#5797)

This is a minor change on top of #5705

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

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

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

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

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

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

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

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

This change excludes unit test code from code coverage reporting.

* refactor: Modularise ledger (#5493)

This change moves the ledger code to libxrpl.

* Mark PermissionDelegation as unsupported

* Set version to 2.6.1-rc2

* Miscellaneous refactors and updates (#5590)

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

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

- Add code coverage for STParsedJSON edge cases

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

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

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

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

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

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

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

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

* Improve ValidatorList invalid UNL manifest logging (#5804)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Rename flags for DynamicMPT (#5820)

* Set version to 2.6.1

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

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

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

* Remove bogus coverage warning (#5838)

* fix return type

---------

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

View File

@@ -98,6 +98,7 @@ find_package(ed25519 REQUIRED)
find_package(gRPC REQUIRED)
find_package(LibArchive REQUIRED)
find_package(lz4 REQUIRED)
find_package(mpt-crypto REQUIRED)
find_package(nudb REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(secp256k1 REQUIRED)
@@ -109,6 +110,7 @@ target_link_libraries(
xrpl_libs
INTERFACE ed25519::ed25519
lz4::lz4
mpt-crypto::mpt-crypto
OpenSSL::Crypto
OpenSSL::SSL
secp256k1::secp256k1

View File

@@ -6,12 +6,13 @@
"sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1765850149.926",
"soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1765850149.46",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
"secp256k1/0.7.1#3a61e95e220062ef32c48d019e9c81f7%1770306721.686",
"secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
"re2/20230301#ca3b241baec15bd31ea9187150e0b333%1765850148.103",
"protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038",
"openssl/3.5.5#05a4ac5b7323f7a329b2db1391d9941f%1769599205.414",
"openssl/3.5.5#05a4ac5b7323f7a329b2db1391d9941f%1770229825.601",
"nudb/2.0.9#0432758a24204da08fee953ec9ea03cb%1769436073.32",
"mpt-crypto/0.1.0-rc2#575de3d495f539e3e5eba957b324d260%1771955268.105",
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
@@ -31,7 +32,7 @@
"strawberryperl/5.32.1.1#707032463aa0620fa17ec0d887f5fe41%1765850165.196",
"protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038",
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
"msys2/cci.latest#eea83308ad7e9023f7318c60d5a9e6cb%1770199879.083",
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
"m4/1.4.19#70dc8bbb33e981d119d2acc0175cf381%1763158052.846",
"cmake/4.2.0#ae0a44f44a1ef9ab68fd4b3e9a1f8671%1765850153.937",
"cmake/3.31.10#313d16a1aa16bbdb2ca0792467214b76%1765850153.479",

View File

@@ -31,6 +31,7 @@ class Xrpl(ConanFile):
"ed25519/2015.03",
"grpc/1.72.0",
"libarchive/3.8.1",
"mpt-crypto/0.1.0-rc2",
"nudb/2.0.9",
"openssl/3.5.5",
"secp256k1/0.7.1",
@@ -209,6 +210,7 @@ class Xrpl(ConanFile):
"grpc::grpc++",
"libarchive::libarchive",
"lz4::lz4",
"mpt-crypto::mpt-crypto",
"nudb::nudb",
"openssl::crypto",
"protobuf::libprotobuf",

View File

@@ -54,6 +54,7 @@ words:
- autobridging
- bimap
- bindir
- blindings
- bookdir
- Bougalis
- Britto
@@ -86,6 +87,7 @@ words:
- daria
- dcmake
- dearmor
- decryptor
- deleteme
- demultiplexer
- deserializaton
@@ -95,6 +97,7 @@ words:
- distro
- doxyfile
- dxrpl
- elgamal
- endmacro
- exceptioned
- Falco
@@ -103,6 +106,7 @@ words:
- fmtdur
- fsanitize
- funclets
- Gamal
- gcov
- gcovr
- ghead
@@ -183,6 +187,7 @@ words:
- partitioner
- paychan
- paychans
- Pedersen
- permdex
- perminute
- permissioned
@@ -218,6 +223,7 @@ words:
- sahyadri
- Satoshi
- scons
- Schnorr
- secp
- sendq
- seqit
@@ -244,6 +250,7 @@ words:
- stvar
- stvector
- stxchainattestations
- summands
- superpeer
- superpeers
- takergets

View File

@@ -0,0 +1,503 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/detail/secp256k1.h>
#include <secp256k1_mpt.h>
namespace xrpl {
/**
* @brief Bundles an ElGamal public key with its associated encrypted amount.
*
* Used to represent a recipient in confidential transfers, containing both
* the recipient's ElGamal public key and the ciphertext encrypting the
* transfer amount under that key.
*/
struct ConfidentialRecipient
{
Slice const publicKey; ///< The recipient's ElGamal public key (64 bytes).
Slice const encryptedAmount; ///< The encrypted amount ciphertext (128 bytes).
};
/**
* @brief Increments the confidential balance version counter on an MPToken.
*
* The version counter is used to prevent replay attacks by binding proofs
* to a specific state of the account's confidential balance. Wraps to 0
* on overflow (defined behavior for unsigned integers).
*
* @param mptoken The MPToken ledger entry to update.
*/
inline void
incrementConfidentialVersion(STObject& mptoken)
{
// Retrieve current version and increment.
// Unsigned integer overflow is defined behavior in C++ (wraps to 0),
// which is acceptable here.
mptoken[sfConfidentialBalanceVersion] = mptoken[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
}
/**
* @brief Adds common fields to a serializer for ZKP context hash generation.
*
* Serializes the transaction type, account, sequence number, and issuance ID
* into the provided serializer. These fields form the base of all context
* hashes used in zero-knowledge proofs.
*
* @param s The serializer to append fields to.
* @param txType The transaction type identifier.
* @param account The account ID of the transaction sender.
* @param sequence The transaction sequence number.
* @param issuanceID The MPToken Issuance ID.
*/
void
addCommonZKPFields(
Serializer& s,
std::uint16_t txType,
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID);
/**
* @brief Generates the context hash for ConfidentialMPTSend transactions.
*
* Creates a unique 256-bit hash that binds the zero-knowledge proofs to
* this specific send transaction, preventing proof reuse across transactions.
*
* @param account The sender's account ID.
* @param sequence The transaction sequence number.
* @param issuanceID The MPToken Issuance ID.
* @param destination The destination account ID.
* @param version The sender's confidential balance version.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getSendContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
AccountID const& destination,
std::uint32_t version);
/**
* @brief Generates the context hash for ConfidentialMPTClawback transactions.
*
* Creates a unique 256-bit hash that binds the equality proof to this
* specific clawback transaction.
*
* @param account The issuer's account ID.
* @param sequence The transaction sequence number.
* @param issuanceID The MPToken Issuance ID.
* @param amount The amount being clawed back.
* @param holder The holder's account ID being clawed back from.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getClawbackContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount,
AccountID const& holder);
/**
* @brief Generates the context hash for ConfidentialMPTConvert transactions.
*
* Creates a unique 256-bit hash that binds the Schnorr proof (for key
* registration) to this specific convert transaction.
*
* @param account The holder's account ID.
* @param sequence The transaction sequence number.
* @param issuanceID The MPToken Issuance ID.
* @param amount The amount being converted to confidential.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getConvertContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount);
/**
* @brief Generates the context hash for ConfidentialMPTConvertBack transactions.
*
* Creates a unique 256-bit hash that binds the zero-knowledge proofs to
* this specific convert-back transaction.
*
* @param account The holder's account ID.
* @param sequence The transaction sequence number.
* @param issuanceID The MPToken Issuance ID.
* @param amount The amount being converted back to public.
* @param version The holder's confidential balance version.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getConvertBackContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount,
std::uint32_t version);
/**
* @brief Parses an ElGamal ciphertext into two secp256k1 public key components.
*
* Breaks a 66-byte encrypted amount (two 33-byte compressed EC points) into
* two secp256k1_pubkey structures (C1, C2) for use in cryptographic operations.
*
* @param buffer The 66-byte buffer containing the compressed ciphertext.
* @param out1 Output: The C1 component of the ElGamal ciphertext.
* @param out2 Output: The C2 component of the ElGamal ciphertext.
* @return true if parsing succeeds, false if the buffer is invalid.
*/
bool
makeEcPair(Slice const& buffer, secp256k1_pubkey& out1, secp256k1_pubkey& out2);
/**
* @brief Serializes two secp256k1 public key components into compressed form.
*
* Converts two secp256k1_pubkey structures (C1, C2) back into a 66-byte
* buffer containing two 33-byte compressed EC points.
*
* @param in1 The C1 component to serialize.
* @param in2 The C2 component to serialize.
* @param buffer Output: The 66-byte buffer to write the compressed ciphertext.
* @return true if serialization succeeds, false otherwise.
*/
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);
/**
* @brief Verifies that a buffer contains a valid, parsable compressed EC point.
*
* Can be used to validate both compressed public keys and Pedersen commitments.
* Fails early if the prefix byte is not 0x02 or 0x03.
*
* @param buffer The input buffer containing a compressed EC point (33 bytes).
* @return true if the point can be parsed successfully, false otherwise.
*/
bool
isValidCompressedECPoint(Slice const& buffer);
/**
* @brief Homomorphically adds two ElGamal ciphertexts.
*
* Uses the additive homomorphic property of ElGamal encryption to compute
* Enc(a + b) from Enc(a) and Enc(b) without decryption.
*
* @param a The first ciphertext (66 bytes).
* @param b The second ciphertext (66 bytes).
* @param out Output: The resulting ciphertext Enc(a + b).
* @return tesSUCCESS on success, or an error code if parsing fails.
*/
TER
homomorphicAdd(Slice const& a, Slice const& b, Buffer& out);
/**
* @brief Homomorphically subtracts two ElGamal ciphertexts.
*
* Uses the additive homomorphic property of ElGamal encryption to compute
* Enc(a - b) from Enc(a) and Enc(b) without decryption.
*
* @param a The minuend ciphertext (66 bytes).
* @param b The subtrahend ciphertext (66 bytes).
* @param out Output: The resulting ciphertext Enc(a - b).
* @return tesSUCCESS on success, or an error code if parsing fails.
*/
TER
homomorphicSubtract(Slice const& a, Slice const& b, Buffer& out);
/**
* @brief Encrypts an amount using ElGamal encryption.
*
* Produces a ciphertext C = (C1, C2) where C1 = r*G and C2 = m*G + r*Pk,
* using the provided blinding factor r.
*
* @param amt The plaintext amount to encrypt.
* @param pubKeySlice The recipient's ElGamal public key (64 bytes).
* @param blindingFactor The 32-byte randomness used as blinding factor r.
* @return The 66-byte ciphertext, or std::nullopt on failure.
*/
std::optional<Buffer>
encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor);
/**
* @brief Generates the canonical zero encryption for a specific MPToken.
*
* Creates a deterministic encryption of zero that is unique to the account
* and MPT issuance. Used to initialize confidential balance fields.
*
* @param pubKeySlice The holder's ElGamal public key (64 bytes).
* @param account The account ID of the token holder.
* @param mptId The MPToken Issuance ID.
* @return The 66-byte canonical zero ciphertext, or std::nullopt on failure.
*/
std::optional<Buffer>
encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId);
/**
* @brief Verifies a Schnorr proof of knowledge of an ElGamal private key.
*
* Proves that the submitter knows the secret key corresponding to the
* provided public key, without revealing the secret key itself.
*
* @param pubKeySlice The ElGamal public key (64 bytes).
* @param proofSlice The Schnorr proof (65 bytes).
* @param contextHash The 256-bit context hash binding the proof.
* @return tesSUCCESS if valid, or an error code otherwise.
*/
TER
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash);
/**
* @brief Verifies that a ciphertext correctly encrypts a revealed amount.
*
* Given the plaintext amount and blinding factor, verifies that the
* ciphertext was correctly constructed using ElGamal encryption.
*
* @param amount The revealed plaintext amount.
* @param blindingFactor The 32-byte blinding factor used in encryption.
* @param pubKeySlice The recipient's ElGamal public key (64 bytes).
* @param ciphertext The ciphertext to verify (66 bytes).
* @return tesSUCCESS if the encryption is valid, or an error code otherwise.
*/
TER
verifyElGamalEncryption(
std::uint64_t const amount,
Slice const& blindingFactor,
Slice const& pubKeySlice,
Slice const& ciphertext);
/**
* @brief Validates the format of encrypted amount fields in a transaction.
*
* Checks that all ciphertext fields in the transaction object have the
* correct length and contain valid EC points. This function is only used
* by ConfidentialMPTConvert and ConfidentialMPTConvertBack transactions.
*
* @param object The transaction object containing encrypted amount fields.
* @return tesSUCCESS if all formats are valid, temMALFORMED if required fields
* are missing, or temBAD_CIPHERTEXT if format validation fails.
*/
NotTEC
checkEncryptedAmountFormat(STObject const& object);
/**
* @brief Verifies revealed amount encryptions for all recipients.
*
* Validates that the same amount was correctly encrypted for the holder,
* issuer, and optionally the auditor using their respective public keys.
*
* @param amount The revealed plaintext amount.
* @param blindingFactor The 32-byte blinding factor used in all encryptions.
* @param holder The holder's public key and encrypted amount.
* @param issuer The issuer's public key and encrypted amount.
* @param auditor Optional auditor's public key and encrypted amount.
* @return tesSUCCESS if all encryptions are valid, or an error code otherwise.
*/
TER
verifyRevealedAmount(
std::uint64_t const amount,
Slice const& blindingFactor,
ConfidentialRecipient const& holder,
ConfidentialRecipient const& issuer,
std::optional<ConfidentialRecipient> const& auditor);
/**
* @brief Returns the number of recipients in a confidential transfer.
*
* Returns 4 if an auditor is present (sender, destination, issuer, auditor),
* or 3 if no auditor (sender, destination, issuer).
*
* @param hasAuditor Whether the issuance has an auditor configured.
* @return The number of recipients (3 or 4).
*/
constexpr std::size_t
getConfidentialRecipientCount(bool hasAuditor)
{
return hasAuditor ? 4 : 3;
}
/**
* @brief Calculates the size of a multi-ciphertext equality proof.
*
* The proof size varies based on the number of recipients because each
* additional recipient requires additional proof components.
*
* @param nRecipients The number of recipients in the transfer.
* @return The size in bytes of the equality proof.
*/
std::size_t
getMultiCiphertextEqualityProofSize(std::size_t nRecipients);
/**
* @brief Verifies a multi-ciphertext equality proof.
*
* Proves that all ciphertexts in the recipients vector encrypt the same
* plaintext amount, without revealing the amount itself.
*
* @param proof The zero-knowledge proof bytes.
* @param recipients Vector of recipients with their public keys and ciphertexts.
* @param nRecipients The number of recipients (must match recipients.size()).
* @param contextHash The 256-bit context hash binding the proof.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyMultiCiphertextEqualityProof(
Slice const& proof,
std::vector<ConfidentialRecipient> const& recipients,
std::size_t const nRecipients,
uint256 const& contextHash);
/**
* @brief Verifies a clawback equality proof.
*
* Proves that the issuer knows the exact amount encrypted in the holder's
* balance ciphertext. Used in ConfidentialMPTClawback to verify the issuer
* can decrypt the balance using their private key.
*
* @param amount The revealed plaintext amount.
* @param proof The zero-knowledge proof bytes.
* @param pubKeySlice The issuer's ElGamal public key (64 bytes).
* @param ciphertext The issuer's encrypted balance on the holder's account (66 bytes).
* @param contextHash The 256-bit context hash binding the proof.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyClawbackEqualityProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
Slice const& ciphertext,
uint256 const& contextHash);
/**
* @brief Generates a cryptographically secure 32-byte blinding factor.
*
* Produces random bytes suitable for use as an ElGamal blinding factor
* or Pedersen commitment randomness.
*
* @return A 32-byte buffer containing the random blinding factor.
*/
Buffer
generateBlindingFactor();
/**
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
* Pedersen Commitment for a transaction Amount.
*
* It proves that the ElGamal ciphertext `encAmt` encrypts the same value `m`
* as the Pedersen Commitment `pcmSlice`, using the randomness `r`.
* Proves Enc(m) <-> Pcm(m)
*
* @param proof The Zero Knowledge Proof bytes.
* @param encAmt The ElGamal ciphertext of the amount (C1, C2).
* @param pubKeySlice The sender's public key.
* @param pcmSlice The Pedersen Commitment to the amount.
* @param contextHash The unique context hash for this transaction.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyAmountPcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash);
/**
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
* Pedersen Commitment for an account Balance.
*
* It proves that the ElGamal ciphertext `encAmt` encrypts the same value `b`
* as the Pedersen Commitment `pcmSlice`, using the secret key `s`.
* Proves Enc(b) <-> Pcm(b)
*
* Note: Swaps arguments (Pk <-> C1) to accommodate the different algebraic
* structure.
*
* @param proof The Zero Knowledge Proof bytes.
* @param encAmt The ElGamal ciphertext of the balance (C1, C2).
* @param pubKeySlice The sender's public key.
* @param pcmSlice The Pedersen Commitment to the balance.
* @param contextHash The unique context hash for this transaction.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyBalancePcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash);
/**
* @brief Verifies an aggregated Bulletproof range proof.
*
* This function verifies that all commitments in commitment_C_vec commit
* to values within the valid 64-bit range [0, 2^64 - 1].
*
* @param proof The serialized Bulletproof proof.
* @param compressedCommitments Vector of compressed Pedersen commitments (each 33 bytes).
* @param contextHash The unique context hash for this transaction.
* @return tesSUCCESS if the proof is valid, tecBAD_PROOF if verification
* fails, or tecINTERNAL for internal errors.
*/
TER
verifyAggregatedBulletproof(
Slice const& proof,
std::vector<Slice> const& compressedCommitments,
uint256 const& contextHash);
/**
* @brief Computes the remainder commitment for ConfidentialMPTSend.
*
* Given a balance commitment PC_bal = m_bal*G + rho_bal*H and an amount
* commitment PC_amt = m_amt*G + rho_amt*H, this function computes:
* PC_rem = PC_bal - PC_amt = (m_bal - m_amt)*G + (rho_bal - rho_amt)*H
*
* This derived commitment is used in an aggregated range proof to ensure
* the sender maintains a non-negative balance (m_bal - m_amt >= 0).
*
* @param balanceCommitment The compressed Pedersen commitment to the balance (33 bytes).
* @param amountCommitment The compressed Pedersen commitment to the amount (33 bytes).
* @param out Output buffer for the resulting remainder commitment (33 bytes).
* @return tesSUCCESS on success, tecINTERNAL on failure.
*/
TER
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out);
/**
* @brief Computes the remainder commitment for ConvertBack.
*
* Given a Pedersen commitment PC = m*G + rho*H, this function computes
* PC_rem = PC - amount*G = (m - amount)*G + rho*H
*
* @param commitment The compressed Pedersen commitment (33 bytes).
* @param amount The amount to subtract (must be non-zero).
* @param out Output buffer for the resulting commitment (33 bytes).
* @return tesSUCCESS on success, tecINTERNAL on failure or if amount is 0.
*/
TER
computeConvertBackRemainder(Slice const& commitment, std::uint64_t amount, Buffer& out);
} // namespace xrpl

View File

@@ -167,7 +167,10 @@ enum LedgerSpecificFlags {
lsfMPTCanTrade = 0x00000010,
lsfMPTCanTransfer = 0x00000020,
lsfMPTCanClawback = 0x00000040,
lsfMPTCanPrivacy = 0x00000080,
// Mutable flags (lsmf prefix) control whether the issuer can change
// corresponding feature flags after issuance via MPTokenIssuanceSet.
lsmfMPTCanMutateCanLock = 0x00000002,
lsmfMPTCanMutateRequireAuth = 0x00000004,
lsmfMPTCanMutateCanEscrow = 0x00000008,
@@ -176,6 +179,12 @@ enum LedgerSpecificFlags {
lsmfMPTCanMutateCanClawback = 0x00000040,
lsmfMPTCanMutateMetadata = 0x00010000,
lsmfMPTCanMutateTransferFee = 0x00020000,
// Controls mutability of lsfMPTCanPrivacy. Note the inverted naming:
// - Other mutable flags: "CanMutate" means issuer CAN change the setting
// - This flag: "CannotMutate" means issuer CANNOT change the setting
// By default (flag not set), issuer can toggle lsfMPTCanPrivacy on/off.
// If set, lsfMPTCanPrivacy is permanently locked to its creation value.
lsmfMPTCannotMutatePrivacy = 0x00040000,
// ltMPTOKEN
lsfMPTAuthorized = 0x00000002,

View File

@@ -296,4 +296,43 @@ 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 in bytes */
std::size_t constexpr ecEqualityProofLength = 98;
/** Length of EC point (compressed) */
std::size_t constexpr compressedECPointLength = 33;
/** Length of EC public key (compressed) */
std::size_t constexpr ecPubKeyLength = compressedECPointLength;
/** Length of EC private key in bytes */
std::size_t constexpr ecPrivKeyLength = 32;
/** Length of the EC blinding factor in bytes */
std::size_t constexpr ecBlindingFactorLength = 32;
/** Length of Schnorr ZKProof for public key registration in bytes */
std::size_t constexpr ecSchnorrProofLength = 65;
/** Length of ElGamal ciphertext equality proof in bytes */
std::size_t constexpr ecCiphertextEqualityProofLength = 261;
/** Length of ElGamal Pedersen linkage proof in bytes */
std::size_t constexpr ecPedersenProofLength = 195;
/** Length of Pedersen Commitment (compressed) */
std::size_t constexpr ecPedersenCommitmentLength = compressedECPointLength;
/** Length of single bulletproof (range proof for 1 commitment) in bytes */
std::size_t constexpr ecSingleBulletproofLength = 688;
/** Length of double bulletproof (range proof for 2 commitments) in bytes */
std::size_t constexpr ecDoubleBulletproofLength = 754;
} // namespace xrpl

View File

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

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

View File

@@ -15,6 +15,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(ConfidentialTransfer, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (BatchInnerSigs, Supported::yes, VoteBehavior::DefaultNo)

View File

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

View File

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

View File

@@ -722,6 +722,8 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
{sfMPTokenMetadata, soeOPTIONAL},
{sfTransferFee, soeOPTIONAL},
{sfMutableFlags, soeOPTIONAL},
{sfIssuerElGamalPublicKey, soeOPTIONAL},
{sfAuditorElGamalPublicKey, soeOPTIONAL},
}))
/** This transaction type authorizes a MPToken instance */
@@ -1058,6 +1060,90 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay,
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This transaction type converts into confidential MPT balance. */
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialMPTConvert.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert,
Delegation::delegable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeREQUIRED},
{sfHolderElGamalPublicKey, soeOPTIONAL},
{sfHolderEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfAuditorEncryptedAmount, soeOPTIONAL},
{sfBlindingFactor, soeREQUIRED},
{sfZKProof, soeOPTIONAL},
}))
/** This transaction type merges MPT inbox. */
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialMPTMergeInbox.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox,
Delegation::delegable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
}))
/** This transaction type converts back into public MPT balance. */
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialMPTConvertBack.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack,
Delegation::delegable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfMPTAmount, soeREQUIRED},
{sfHolderEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfAuditorEncryptedAmount, soeOPTIONAL},
{sfBlindingFactor, soeREQUIRED},
{sfZKProof, soeREQUIRED},
{sfBalanceCommitment, soeREQUIRED},
}))
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialMPTSend.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend,
Delegation::delegable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfDestination, soeREQUIRED},
{sfSenderEncryptedAmount, soeREQUIRED},
{sfDestinationEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfAuditorEncryptedAmount, soeOPTIONAL},
{sfZKProof, soeREQUIRED},
{sfAmountCommitment, soeREQUIRED},
{sfBalanceCommitment, soeREQUIRED},
{sfCredentialIDs, soeOPTIONAL},
}))
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialMPTClawback.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback,
Delegation::delegable,
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

@@ -468,7 +468,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))
if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED &&
(view.rules().enabled(featureSingleAssetVault) || view.rules().enabled(featureConfidentialTransfer)))
{
if (auto const err = requireAuth(view, mptIssue, account, AuthType::StrongAuth); !isTesSuccess(err))
amount.clear(mptIssue);

View File

@@ -0,0 +1,659 @@
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Protocol.h>
#include <boost/endian/conversion.hpp>
#include <openssl/rand.h>
#include <openssl/sha.h>
namespace xrpl {
void
addCommonZKPFields(
Serializer& s,
std::uint16_t txType,
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID)
{
s.add16(txType);
s.addBitString(account);
s.add32(sequence);
s.addBitString(issuanceID);
}
uint256
getSendContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
AccountID const& destination,
std::uint32_t version)
{
Serializer s;
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_SEND, account, sequence, issuanceID);
s.addBitString(destination);
s.addInteger(version);
return s.getSHA512Half();
}
uint256
getClawbackContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount,
AccountID const& holder)
{
Serializer s;
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CLAWBACK, account, sequence, issuanceID);
s.add64(amount);
s.addBitString(holder);
return s.getSHA512Half();
}
uint256
getConvertContextHash(AccountID const& account, std::uint32_t sequence, uint192 const& issuanceID, std::uint64_t amount)
{
Serializer s;
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT, account, sequence, issuanceID);
s.add64(amount);
return s.getSHA512Half();
}
uint256
getConvertBackContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount,
std::uint32_t version)
{
Serializer s;
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT_BACK, account, sequence, issuanceID);
s.add64(amount);
s.addInteger(version);
return s.getSHA512Half();
}
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)
{
secp256k1_pubkey key1;
secp256k1_pubkey key2;
return makeEcPair(buffer, key1, key2);
}
bool
isValidCompressedECPoint(Slice const& buffer)
{
if (buffer.size() != compressedECPointLength)
return false;
// Compressed EC points must start with 0x02 or 0x03
if (buffer[0] != 0x02 && buffer[0] != 0x03)
return false;
secp256k1_pubkey point;
return secp256k1_ec_pubkey_parse(secp256k1Context(), &point, buffer.data(), buffer.size()) == 1;
}
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;
}
Buffer
generateBlindingFactor()
{
unsigned char blindingFactor[ecBlindingFactorLength];
// todo: might need to be updated using another RNG
if (RAND_bytes(blindingFactor, ecBlindingFactorLength) != 1)
Throw<std::runtime_error>("Failed to generate random number");
return Buffer(blindingFactor, ecBlindingFactorLength);
}
std::optional<Buffer>
encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor)
{
if (blindingFactor.size() != ecBlindingFactorLength)
return std::nullopt;
if (pubKeySlice.size() != ecPubKeyLength)
return std::nullopt;
secp256k1_pubkey c1, c2, pubKey;
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
return std::nullopt;
if (!secp256k1_elgamal_encrypt(secp256k1Context(), &c1, &c2, &pubKey, amt, blindingFactor.data()))
return std::nullopt;
Buffer buf(ecGamalEncryptedTotalLength);
if (!serializeEcPair(c1, c2, buf))
return std::nullopt;
return buf;
}
std::optional<Buffer>
encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId)
{
if (pubKeySlice.size() != ecPubKeyLength)
return std::nullopt; // LCOV_EXCL_LINE
secp256k1_pubkey c1, c2, pubKey;
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
return std::nullopt; // LCOV_EXCL_LINE
if (!generate_canonical_encrypted_zero(secp256k1Context(), &c1, &c2, &pubKey, account.data(), mptId.data()))
return std::nullopt; // LCOV_EXCL_LINE
Buffer buf(ecGamalEncryptedTotalLength);
if (!serializeEcPair(c1, c2, buf))
return std::nullopt; // LCOV_EXCL_LINE
return buf;
}
TER
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash)
{
if (proofSlice.size() != ecSchnorrProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
if (secp256k1_mpt_pok_sk_verify(secp256k1Context(), proofSlice.data(), &pubKey, contextHash.data()) != 1)
return tecBAD_PROOF;
return tesSUCCESS;
}
TER
verifyElGamalEncryption(
std::uint64_t const amount,
Slice const& blindingFactor,
Slice const& pubKeySlice,
Slice const& ciphertext)
{
if (blindingFactor.size() != ecBlindingFactorLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey c1, c2;
if (!makeEcPair(ciphertext, c1, c2))
return tecINTERNAL; // LCOV_EXCL_LINE
if (secp256k1_elgamal_verify_encryption(secp256k1Context(), &c1, &c2, &pubKey, amount, blindingFactor.data()) != 1)
return tecBAD_PROOF;
return tesSUCCESS;
}
TER
verifyRevealedAmount(
std::uint64_t const amount,
Slice const& blindingFactor,
ConfidentialRecipient const& holder,
ConfidentialRecipient const& issuer,
std::optional<ConfidentialRecipient> const& auditor)
{
if (auto const res = verifyElGamalEncryption(amount, blindingFactor, holder.publicKey, holder.encryptedAmount);
!isTesSuccess(res))
{
return res;
}
if (auto const res = verifyElGamalEncryption(amount, blindingFactor, issuer.publicKey, issuer.encryptedAmount);
!isTesSuccess(res))
{
return res;
}
if (auditor)
{
if (auto const res =
verifyElGamalEncryption(amount, blindingFactor, auditor->publicKey, auditor->encryptedAmount);
!isTesSuccess(res))
{
return res;
}
}
return tesSUCCESS;
}
std::size_t
getMultiCiphertextEqualityProofSize(std::size_t nRecipients)
{
// Points (33 bytes): T_m (1) + T_rG (nRecipients) + T_rP (nRecipients) = 1
// + 2nRecipients Scalars (32 bytes): s_m (1) + s_r (nRecipients) = 1 +
// nRecipients
return ((1 + (2 * nRecipients)) * 33) + ((1 + nRecipients) * 32);
}
TER
verifyMultiCiphertextEqualityProof(
Slice const& proof,
std::vector<ConfidentialRecipient> const& recipients,
std::size_t const nRecipients,
uint256 const& contextHash)
{
if (recipients.size() != nRecipients)
return tecINTERNAL; // LCOV_EXCL_LINE
if (proof.size() != getMultiCiphertextEqualityProofSize(nRecipients))
return tecINTERNAL; // LCOV_EXCL_LINE
std::vector<secp256k1_pubkey> r(nRecipients);
std::vector<secp256k1_pubkey> s(nRecipients);
std::vector<secp256k1_pubkey> pk(nRecipients);
for (size_t i = 0; i < nRecipients; ++i)
{
auto const& recipient = recipients[i];
if (recipient.encryptedAmount.size() != ecGamalEncryptedTotalLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (!makeEcPair(recipient.encryptedAmount, r[i], s[i]))
return tecINTERNAL; // LCOV_EXCL_LINE
if (recipient.publicKey.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pk[i], recipient.publicKey.data(), ecPubKeyLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
}
int const result = secp256k1_mpt_verify_same_plaintext_multi(
secp256k1Context(), proof.data(), proof.size(), nRecipients, r.data(), s.data(), pk.data(), contextHash.data());
if (result != 1)
return tecBAD_PROOF;
return tesSUCCESS;
}
TER
verifyClawbackEqualityProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
Slice const& ciphertext,
uint256 const& contextHash)
{
secp256k1_pubkey c1, c2;
if (!makeEcPair(ciphertext, c1, c2))
return tecINTERNAL; // LCOV_EXCL_LINE
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
// Note: c2, c1 order - the proof is generated with c2 first (the encrypted
// message component) because the equality proof structure expects the
// message-containing term before the blinding term.
if (secp256k1_equality_plaintext_verify(
secp256k1Context(), proof.data(), &pubKey, &c2, &c1, amount, contextHash.data()) != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
NotTEC
checkEncryptedAmountFormat(STObject const& object)
{
// Current usage of this function is only for ConfidentialMPTConvert and
// ConfidentialMPTConvertBack transactions, which already enforce that these fields
// are present.
if (!object.isFieldPresent(sfHolderEncryptedAmount) || !object.isFieldPresent(sfIssuerEncryptedAmount))
return temMALFORMED; // LCOV_EXCL_LINE
if (object[sfHolderEncryptedAmount].length() != ecGamalEncryptedTotalLength ||
object[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
bool const hasAuditor = object.isFieldPresent(sfAuditorEncryptedAmount);
if (hasAuditor && object[sfAuditorEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (!isValidCiphertext(object[sfHolderEncryptedAmount]) || !isValidCiphertext(object[sfIssuerEncryptedAmount]))
return temBAD_CIPHERTEXT;
if (hasAuditor && !isValidCiphertext(object[sfAuditorEncryptedAmount]))
return temBAD_CIPHERTEXT;
return tesSUCCESS;
}
TER
verifyAmountPcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash)
{
if (proof.length() != ecPedersenProofLength)
return tecINTERNAL;
secp256k1_pubkey c1, c2;
if (!makeEcPair(encAmt, c1, c2))
return tecINTERNAL; // LCOV_EXCL_LINE
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pcmSlice.size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pcm;
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
if (secp256k1_elgamal_pedersen_link_verify(
secp256k1Context(), proof.data(), &c1, &c2, &pubKey, &pcm, contextHash.data()) != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyBalancePcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash)
{
if (proof.length() != ecPedersenProofLength)
return tecINTERNAL;
secp256k1_pubkey c1;
secp256k1_pubkey c2;
if (!makeEcPair(encAmt, c1, c2))
return tecINTERNAL; // LCOV_EXCL_LINE
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pcmSlice.size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pcm;
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
// Note: c2, c1 order - the linkage proof expects the message-containing
// component (c2 = m*G + r*Pk) before the blinding component (c1 = r*G).
if (secp256k1_elgamal_pedersen_link_verify(
secp256k1Context(), proof.data(), &pubKey, &c2, &c1, &pcm, contextHash.data()) != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyAggregatedBulletproof(
Slice const& proof,
std::vector<Slice> const& compressedCommitments,
uint256 const& contextHash)
{
// 1. Validate Aggregation Factor (m), m to be a power of 2
std::size_t const m = compressedCommitments.size();
if (m == 0 || (m & (m - 1)) != 0)
return tecINTERNAL; // LCOV_EXCL_LINE
// 2. Prepare Pedersen Commitments, parse from compressed format
auto const ctx = secp256k1Context();
std::vector<secp256k1_pubkey> commitments(m);
for (size_t i = 0; i < m; ++i)
{
// Sanity check length
if (compressedCommitments[i].size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (secp256k1_ec_pubkey_parse(
ctx, &commitments[i], compressedCommitments[i].data(), ecPedersenCommitmentLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
}
// 3. Prepare Generator Vectors (G_vec, H_vec)
// The range proof requires vectors of size 64 * m
std::size_t const n = 64 * m;
std::vector<secp256k1_pubkey> G_vec(n);
std::vector<secp256k1_pubkey> H_vec(n);
// Retrieve deterministic generators "G" and "H"
if (secp256k1_mpt_get_generator_vector(ctx, G_vec.data(), n, (unsigned char const*)"G", 1) != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (secp256k1_mpt_get_generator_vector(ctx, H_vec.data(), n, (unsigned char const*)"H", 1) != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// 4. Prepare Base Generator (pk_base / H)
secp256k1_pubkey pk_base;
if (secp256k1_mpt_get_h_generator(ctx, &pk_base) != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// 5. Verify the Proof
int const result = secp256k1_bulletproof_verify_agg(
ctx,
G_vec.data(),
H_vec.data(),
reinterpret_cast<unsigned char const*>(proof.data()),
proof.size(),
commitments.data(),
m,
&pk_base,
contextHash.data());
if (result != 1)
return tecBAD_PROOF;
return tesSUCCESS;
}
TER
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out)
{
if (balanceCommitment.size() != ecPedersenCommitmentLength || amountCommitment.size() != ecPedersenCommitmentLength)
return tecINTERNAL;
auto const ctx = secp256k1Context();
secp256k1_pubkey pcBalance;
if (secp256k1_ec_pubkey_parse(ctx, &pcBalance, balanceCommitment.data(), ecPedersenCommitmentLength) != 1)
return tecINTERNAL;
secp256k1_pubkey pcAmount;
if (secp256k1_ec_pubkey_parse(ctx, &pcAmount, amountCommitment.data(), ecPedersenCommitmentLength) != 1)
return tecINTERNAL;
// Negate PC_amount point to get -PC_amount
if (!secp256k1_ec_pubkey_negate(ctx, &pcAmount))
return tecINTERNAL;
// Compute pcRem = pcBalance + (-pcAmount)
secp256k1_pubkey const* summands[2] = {&pcBalance, &pcAmount};
secp256k1_pubkey pcRem;
if (!secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2))
return tecINTERNAL;
// Serialize result to compressed format
out.alloc(ecPedersenCommitmentLength);
size_t outLen = ecPedersenCommitmentLength;
if (secp256k1_ec_pubkey_serialize(ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED) != 1)
return tecINTERNAL;
return tesSUCCESS;
}
TER
computeConvertBackRemainder(Slice const& commitment, std::uint64_t amount, Buffer& out)
{
if (commitment.size() != ecPedersenCommitmentLength || amount == 0)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ctx = secp256k1Context();
// Parse commitment from compressed format
secp256k1_pubkey pcBalance;
if (secp256k1_ec_pubkey_parse(ctx, &pcBalance, commitment.data(), ecPedersenCommitmentLength) != 1)
return tecINTERNAL; // LCOV_EXCL_LINE
// Convert amount to 32-byte big-endian scalar
unsigned char mScalar[32] = {0};
std::uint64_t amountBigEndian = boost::endian::native_to_big(amount);
std::memcpy(&mScalar[24], &amountBigEndian, sizeof(amountBigEndian));
// Compute mG = amount * G
secp256k1_pubkey mG;
if (!secp256k1_ec_pubkey_create(ctx, &mG, mScalar))
return tecINTERNAL; // LCOV_EXCL_LINE
// Negate mG to get -mG
if (!secp256k1_ec_pubkey_negate(ctx, &mG))
return tecINTERNAL; // LCOV_EXCL_LINE
// Compute pcRem = pcBalance + (-mG)
secp256k1_pubkey const* summands[2] = {&pcBalance, &mG};
secp256k1_pubkey pcRem;
if (!secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2))
return tecINTERNAL; // LCOV_EXCL_LINE
// Serialize result to compressed format
out.alloc(ecPedersenCommitmentLength);
size_t outLen = ecPedersenCommitmentLength;
if (secp256k1_ec_pubkey_serialize(ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED) != 1 ||
outLen != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -106,6 +106,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."),
@@ -198,6 +199,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

@@ -3707,6 +3707,189 @@ class Invariants_test : public beast::unit_test::suite
precloseMpt);
}
void
testConfidentialMPTTransfer()
{
using namespace test::jtx;
testcase << "ValidConfidentialMPToken";
MPTID mptID;
// Generate an MPT with privacy, issue 100 tokens to A2.
// Perform a confidential conversion to populate encrypted state.
auto const precloseConfidential = [&mptID](Account const& A1, Account const& A2, Env& env) -> bool {
MPTTester mpt(env, A1, {.holders = {A2}, .fund = false});
mpt.create({.flags = tfMPTCanTransfer | tfMPTCanPrivacy});
mptID = mpt.issuanceID();
mpt.authorize({.account = A2});
mpt.pay(A1, A2, 100);
mpt.generateKeyPair(A1);
mpt.set({.account = A1, .issuerPubKey = mpt.getPubKey(A1)});
mpt.generateKeyPair(A2);
mpt.convert({
.account = A2,
.amt = 100,
.holderPubKey = mpt.getPubKey(A2),
});
return true;
};
// badDelete
doInvariantCheck(
{"MPToken deleted with encrypted fields while COA > 0"},
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
if (!sleToken)
return false;
// Force an erase of the object while the COA remains 100
ac.view().erase(sleToken);
return true;
},
XRPAmount{},
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseConfidential);
// badConsistency
doInvariantCheck(
{"MPToken encrypted field existence inconsistency"},
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
if (!sleToken)
return false;
// Remove one of the required encrypted fields to create a mismatch
sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
ac.view().update(sleToken);
return true;
},
XRPAmount{},
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseConfidential);
// requiresPrivacyFlag
auto const precloseNoPrivacy = [&mptID](Account const& A1, Account const& A2, Env& env) -> bool {
MPTTester mpt(env, A1, {.holders = {A2}, .fund = false});
// completely omitted the tfMPTCanPrivacy flag here.
mpt.create({.flags = tfMPTCanTransfer});
mptID = mpt.issuanceID();
mpt.authorize({.account = A2});
mpt.pay(A1, A2, 100);
return true;
};
doInvariantCheck(
{"MPToken has encrypted fields but Issuance does not have lsfMPTCanPrivacy set"},
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
if (!sleToken)
return false;
// Inject fields correctly, but the Issuance was built without the privacy flag.
sleToken->setFieldVL(sfConfidentialBalanceInbox, Blob{0x00});
sleToken->setFieldVL(sfIssuerEncryptedBalance, Blob{0x00});
ac.view().update(sleToken);
return true;
},
XRPAmount{},
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseNoPrivacy);
// badCOA
doInvariantCheck(
{"Confidential outstanding amount exceeds total outstanding amount"},
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleIssuance = ac.view().peek(keylet::mptIssuance(mptID));
if (!sleIssuance)
return false;
// Total outstanding is natively 100; bloat the COA over 100
sleIssuance->setFieldU64(sfConfidentialOutstandingAmount, 200);
ac.view().update(sleIssuance);
return true;
},
XRPAmount{},
STTx{ttMPTOKEN_ISSUANCE_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseConfidential);
// Conservation Violation
doInvariantCheck(
{"Token conservation violation for MPT"},
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleIssuance = ac.view().peek(keylet::mptIssuance(mptID));
if (!sleIssuance)
return false;
sleIssuance->setFieldU64(
sfConfidentialOutstandingAmount, sleIssuance->getFieldU64(sfConfidentialOutstandingAmount) - 10);
ac.view().update(sleIssuance);
return true;
},
XRPAmount{},
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseConfidential);
// badVersion
doInvariantCheck(
{"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending changed"},
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
if (!sleToken)
return false;
sleToken->setFieldVL(sfConfidentialBalanceSpending, Blob{0xBA, 0xDD});
// DO NOT update sfConfidentialBalanceVersion
ac.view().update(sleToken);
return true;
},
XRPAmount{},
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseConfidential);
// Skipping Deleted MPTs (Issuance deleted)
auto const precloseOrphan = [&mptID](Account const& A1, Account const& A2, Env& env) -> bool {
MPTTester mpt(env, A1, {.holders = {A2}, .fund = false});
mpt.create({.flags = tfMPTCanTransfer | tfMPTCanPrivacy});
mptID = mpt.issuanceID();
mpt.authorize({.account = A2});
// Generate privacy keys and convert 0 amount so Bob has the encrypted fields
mpt.generateKeyPair(A1);
mpt.set({.account = A1, .issuerPubKey = mpt.getPubKey(A1)});
mpt.generateKeyPair(A2);
mpt.convert({
.account = A2,
.amt = 0,
.holderPubKey = mpt.getPubKey(A2),
});
// Immediately destroy the issuance. A2's empty, encrypted token object lives on.
mpt.destroy();
return true;
};
doInvariantCheck(
{},
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
if (!sleToken)
return false;
// Safely able to erase the deleted token.
ac.view().erase(sleToken);
return true;
},
XRPAmount{},
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
{tesSUCCESS, tesSUCCESS},
precloseOrphan);
}
public:
void
run() override
@@ -3732,6 +3915,7 @@ public:
testValidPseudoAccounts();
testValidLoanBroker();
testVault();
testConfidentialMPTTransfer();
}
};

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,11 @@
#include <test/jtx/ter.h>
#include <test/jtx/txflags.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
namespace xrpl {
namespace test {
namespace jtx {
@@ -15,6 +18,22 @@ class MPTTester;
auto const MPTDEXFlags = tfMPTCanTrade | tfMPTCanTransfer;
/*Helper lambda to create a zero-initialized buffer.
WHY THIS IS NEEDED: In C++, xrpl::Buffer(size) allocates uninitialized heap memory.
Because CI runs unit tests sequentially in the same process, uninitialized memory
often recycles "ghost data" (like valid SECP256k1 keys or Pedersen commitments)
left over from previously executed tests.
When testing malformed cryptography paths, passing uninitialized memory might
accidentally supply a valid curve point, causing the ledger's preflight checks
to falsely succeed and return tecBAD_PROOF instead of the expected temMALFORMED.
Explicitly zeroing the buffer guarantees it fails structural validation. */
static auto makeZeroBuffer = [](size_t size) {
Buffer b(size);
if (size > 0)
std::memset(b.data(), 0, size);
return b;
};
// Check flags settings on MPT create
class mptflags
{
@@ -93,6 +112,7 @@ struct MPTCreate
struct MPTInit
{
Holders holders = {};
std::optional<Account> auditor = std::nullopt;
PrettyAmount const xrp = XRP(10'000);
PrettyAmount const xrpHolders = XRP(10'000);
bool fund = true;
@@ -107,6 +127,7 @@ struct MPTInitDef
Env& env;
Account issuer;
Holders holders = {};
std::optional<Account> auditor = std::nullopt;
std::uint16_t transferFee = 0;
std::optional<std::uint64_t> pay = std::nullopt;
std::uint32_t flags = MPTDEXFlags;
@@ -151,18 +172,131 @@ struct MPTSet
std::optional<std::string> metadata = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<uint256> domainID = std::nullopt;
std::optional<Buffer> issuerPubKey = std::nullopt;
std::optional<Buffer> auditorPubKey = 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<bool> fillAuditorEncryptedAmt = true;
// indicates whether to autofill schnorr proof.
// default : auto generate proof if holderPubKey is present.
// true: force proof generation.
// false: force proof omission.
std::optional<bool> fillSchnorrProof = std::nullopt;
std::optional<Buffer> holderPubKey = std::nullopt;
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
std::optional<Buffer> auditorEncryptedAmt = std::nullopt;
std::optional<Buffer> blindingFactor = 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<Buffer> auditorEncryptedAmt = std::nullopt;
std::optional<std::vector<std::string>> credentials = std::nullopt;
// not an txn param, only used for autofilling
std::optional<Buffer> blindingFactor = std::nullopt;
std::optional<Buffer> amountCommitment = std::nullopt;
std::optional<Buffer> balanceCommitment = 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<Buffer> proof = std::nullopt;
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
std::optional<Buffer> auditorEncryptedAmt = std::nullopt;
std::optional<bool> fillAuditorEncryptedAmt = true;
// not an txn param, only used for autofilling
std::optional<Buffer> blindingFactor = std::nullopt;
std::optional<Buffer> pedersenCommitment = 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;
};
/**
* @brief Stores the parameters that are exclusively used to generate a
* pedersen linkage proof
*/
struct PedersenProofParams
{
Buffer const pedersenCommitment;
uint64_t const amt; // either spending balance or value to be transferred
Buffer const encryptedAmt;
Buffer const blindingFactor;
};
class MPTTester
{
Env& env_;
Account const issuer_;
std::unordered_map<std::string, Account> const holders_;
std::optional<Account> const auditor_;
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,
AUDITOR_ENCRYPTED_BALANCE,
};
MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {});
MPTTester(MPTInitDef const& constr);
MPTTester(
@@ -200,6 +334,21 @@ public:
static Json::Value
setJV(MPTSet const& set = {});
void
convert(MPTConvert const& arg = MPTConvert{});
void
mergeInbox(MPTMergeInbox const& arg = MPTMergeInbox{});
void
send(MPTConfidentialSend const& arg = MPTConfidentialSend{});
void
convertBack(MPTConvertBack const& arg = MPTConvertBack{});
void
confidentialClaw(MPTConfidentialClawback const& arg = MPTConfidentialClawback{});
[[nodiscard]] bool
checkDomainID(std::optional<uint256> expected) const;
@@ -209,6 +358,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, std::optional<Account> const& holder = std::nullopt) const;
@@ -229,6 +381,7 @@ public:
{
return issuer_;
}
Account const&
holder(std::string const& h) const;
@@ -256,6 +409,12 @@ 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;
@@ -264,6 +423,81 @@ public:
operator Asset() const;
bool
printMPT(Account const& holder_) const;
void
generateKeyPair(Account const& account);
std::optional<Buffer>
getPubKey(Account const& account) const;
std::optional<Buffer>
getPrivKey(Account const& account) const;
Buffer
encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor) const;
std::optional<uint64_t>
decryptAmount(Account const& account, Buffer const& amt) const;
std::optional<uint64_t>
getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const;
std::int64_t
getIssuanceOutstandingBalance() const;
std::optional<Buffer>
getClawbackProof(Account const& holder, std::uint64_t amount, Buffer const& privateKey, uint256 const& txHash)
const;
std::optional<Buffer>
getSchnorrProof(Account const& account, uint256 const& ctxHash) const;
std::optional<Buffer>
getConfidentialSendProof(
Account const& sender,
std::uint64_t const amount,
std::vector<ConfidentialRecipient> const& recipients,
Slice const& blindingFactor,
std::size_t const nRecipients,
uint256 const& contextHash,
PedersenProofParams const& amountParams,
PedersenProofParams const& balanceParams) const;
Buffer
getConvertBackProof(
Account const& holder,
std::uint64_t const amount,
uint256 const& contextHash,
PedersenProofParams const& pcParams) const;
std::uint32_t
getMPTokenVersion(Account const account) const;
Buffer
getAmountLinkageProof(
Buffer const& pubKey,
Buffer const& blindingFactor,
uint256 const& contextHash,
PedersenProofParams const& params) const;
Buffer
getBalanceLinkageProof(
Account const& account,
uint256 const& contextHash,
Buffer const& pubKey,
PedersenProofParams const& params) const;
Buffer
getBulletproof(
std::vector<std::uint64_t> const& values,
std::vector<Buffer> const& blindingFactors,
uint256 const& contextHash) const;
Buffer
getPedersenCommitment(std::uint64_t const amount, Buffer const& pedersenBlindingFactor);
private:
using SLEP = SLE::const_pointer;
bool
@@ -293,6 +527,16 @@ private:
std::uint32_t
getFlags(std::optional<Account> const& holder) const;
template <typename T>
void
fillConversionCiphertexts(
T const& arg,
Json::Value& jv,
Buffer& holderCiphertext,
Buffer& issuerCiphertext,
std::optional<Buffer>& auditorCiphertext,
Buffer& blindingFactor) const;
};
} // namespace jtx

View File

@@ -51,6 +51,11 @@ public:
ttLOAN_DELETE,
ttLOAN_MANAGE,
ttLOAN_PAY,
ttCONFIDENTIAL_MPT_SEND,
ttCONFIDENTIAL_MPT_CONVERT,
ttCONFIDENTIAL_MPT_CONVERT_BACK,
ttCONFIDENTIAL_MPT_MERGE_INBOX,
ttCONFIDENTIAL_MPT_CLAWBACK,
});
};

View File

@@ -0,0 +1,160 @@
#include <xrpld/app/tx/detail/ConfidentialMPTClawback.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 xrpl {
NotTEC
ConfidentialMPTClawback::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
auto const account = ctx.tx[sfAccount];
// Only issuer can clawback
if (account != MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer())
return temMALFORMED;
// Cannot clawback from self
if (account == ctx.tx[sfHolder])
return temMALFORMED;
// Check invalid claw amount
auto const clawAmount = ctx.tx[sfMPTAmount];
if (clawAmount == 0 || clawAmount > maxMPTokenAmount)
return temBAD_AMOUNT;
// Verify proof length
if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
return temMALFORMED;
return tesSUCCESS;
}
TER
ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx)
{
// Check if sender account exists
auto const account = ctx.tx[sfAccount];
if (!ctx.view.exists(keylet::account(account)))
return terNO_ACCOUNT;
// Check if holder account exists
auto const holder = ctx.tx[sfHolder];
if (!ctx.view.exists(keylet::account(holder)))
return tecNO_TARGET;
// Check if MPT issuance exists
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// Sanity check: issuer must be the same as account
if (sleIssuance->getAccountID(sfIssuer) != account)
return tefINTERNAL; // LCOV_EXCL_LINE
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;
// Check if clawback is allowed
if (!sleIssuance->isFlag(lsfMPTCanClawback))
return tecNO_PERMISSION;
// Check holder's MPToken
auto const sleHolderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, holder));
if (!sleHolderMPToken)
return tecOBJECT_NOT_FOUND;
// Check if holder has confidential balances to claw back
if (!sleHolderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
return tecNO_PERMISSION;
// Sanity check: claw amount can not exceed confidential outstanding amount
auto const amount = ctx.tx[sfMPTAmount];
if (amount > (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0))
return tecINSUFFICIENT_FUNDS;
auto const contextHash = getClawbackContextHash(account, ctx.tx[sfSequence], mptIssuanceID, amount, holder);
// Verify the revealed confidential amount by the issuer matches the exact
// confidential balance of the holder.
return verifyClawbackEqualityProof(
amount,
ctx.tx[sfZKProof],
(*sleIssuance)[sfIssuerElGamalPublicKey],
(*sleHolderMPToken)[sfIssuerEncryptedBalance],
contextHash);
}
TER
ConfidentialMPTClawback::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; // LCOV_EXCL_LINE
auto const clawAmount = ctx_.tx[sfMPTAmount];
Slice const holderPubKey = (*sleHolderMPToken)[sfHolderElGamalPublicKey];
Slice const issuerPubKey = (*sleIssuance)[sfIssuerElGamalPublicKey];
// After clawback, the balance should be encrypted zero.
auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
if (!encZeroForHolder)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const encZeroForIssuer = encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID);
if (!encZeroForIssuer)
return tecINTERNAL; // LCOV_EXCL_LINE
// Set holder's confidential balances to encrypted zero
(*sleHolderMPToken)[sfConfidentialBalanceInbox] = *encZeroForHolder;
(*sleHolderMPToken)[sfConfidentialBalanceSpending] = *encZeroForHolder;
(*sleHolderMPToken)[sfIssuerEncryptedBalance] = *encZeroForIssuer;
(*sleHolderMPToken)[sfConfidentialBalanceVersion] = 0;
if (sleHolderMPToken->isFieldPresent(sfAuditorEncryptedBalance))
{
// Sanity check: the issuance must have an auditor public key if
// auditing is enabled.
if (!sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey))
return tecINTERNAL; // LCOV_EXCL_LINE
Slice const auditorPubKey = (*sleIssuance)[sfAuditorElGamalPublicKey];
auto const encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID);
if (!encZeroForAuditor)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleHolderMPToken)[sfAuditorEncryptedBalance] = *encZeroForAuditor;
}
// 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 xrpl

View File

@@ -0,0 +1,42 @@
#pragma once
#include <xrpld/app/tx/detail/Transactor.h>
namespace xrpl {
/**
* @brief Allows an MPT issuer to clawback confidential balances from a holder.
*
* This transaction enables the issuer of an MPToken Issuance (with clawback
* enabled) to reclaim confidential tokens from a holder's account. Unlike
* regular clawback, the issuer cannot see the holder's balance directly.
* Instead, the issuer must provide a zero-knowledge proof that demonstrates
* they know the exact encrypted balance amount.
*
* @par Cryptographic Operations:
* - **Equality Proof Verification**: Verifies that the issuer's revealed
* amount matches the holder's encrypted balance using the issuer's
* ElGamal private key.
*
* @see ConfidentialMPTSend, ConfidentialMPTConvert
*/
class ConfidentialMPTClawback : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTClawback(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,254 @@
#include <xrpld/app/tx/detail/ConfidentialMPTConvert.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 xrpl {
NotTEC
ConfidentialMPTConvert::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[sfMPTAmount] > maxMPTokenAmount)
return temBAD_AMOUNT;
if (ctx.tx[sfBlindingFactor].size() != ecBlindingFactorLength)
return temMALFORMED;
if (ctx.tx.isFieldPresent(sfHolderElGamalPublicKey))
{
if (!isValidCompressedECPoint(ctx.tx[sfHolderElGamalPublicKey]))
return temMALFORMED;
// proof of knowledge of the secret key corresponding to the provided
// public key is needed when holder ec public key is being set.
if (!ctx.tx.isFieldPresent(sfZKProof))
return temMALFORMED;
// verify schnorr proof length when registerring holder ec public key
if (ctx.tx[sfZKProof].size() != ecSchnorrProofLength)
return temMALFORMED;
}
else
{
// zkp should not be present if public key was already set
if (ctx.tx.isFieldPresent(sfZKProof))
return temMALFORMED;
}
// check encrypted amount format after the above basic checks
// this check is more expensive so put it at the end
if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
return res;
return tesSUCCESS;
}
TER
ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx)
{
auto const account = ctx.tx[sfAccount];
auto const issuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const amount = ctx.tx[sfMPTAmount];
// ensure that issuance exists
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(issuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == account)
return tefINTERNAL; // LCOV_EXCL_LINE
// issuer has not uploaded their pub key yet
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey);
// tx must include auditor ciphertext if the issuance has enabled
// auditing, and must not include it if auditing is not enabled
if (requiresAuditor != hasAuditor)
return tecNO_PERMISSION;
auto const sleMptoken = ctx.view.read(keylet::mptoken(issuanceID, account));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
auto const mptIssue = MPTIssue{issuanceID};
STAmount const mptAmount = STAmount(MPTAmount{static_cast<MPTAmount::value_type>(amount)}, mptIssue);
if (accountHolds(
ctx.view,
account,
mptIssue,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,
ctx.j) < mptAmount)
{
return tecINSUFFICIENT_FUNDS;
}
auto const hasHolderKeyOnLedger = sleMptoken->isFieldPresent(sfHolderElGamalPublicKey);
auto const hasHolderKeyInTx = ctx.tx.isFieldPresent(sfHolderElGamalPublicKey);
// must have pk to convert
if (!hasHolderKeyOnLedger && !hasHolderKeyInTx)
return tecNO_PERMISSION;
// can't update if there's already a pk
if (hasHolderKeyOnLedger && hasHolderKeyInTx)
return tecDUPLICATE;
Slice holderPubKey;
if (hasHolderKeyInTx)
{
holderPubKey = ctx.tx[sfHolderElGamalPublicKey];
auto const contextHash = getConvertContextHash(account, ctx.tx[sfSequence], issuanceID, amount);
// when register new pk, verify through schnorr proof
if (!isTesSuccess(verifySchnorrProof(holderPubKey, ctx.tx[sfZKProof], contextHash)))
{
return tecBAD_PROOF;
}
}
else
{
holderPubKey = (*sleMptoken)[sfHolderElGamalPublicKey];
}
std::optional<ConfidentialRecipient> auditor;
if (hasAuditor)
{
auditor.emplace(
ConfidentialRecipient{(*sleIssuance)[sfAuditorElGamalPublicKey], ctx.tx[sfAuditorEncryptedAmount]});
}
return verifyRevealedAmount(
amount,
ctx.tx[sfBlindingFactor],
{holderPubKey, ctx.tx[sfHolderEncryptedAmount]},
{(*sleIssuance)[sfIssuerElGamalPublicKey], ctx.tx[sfIssuerEncryptedAmount]},
auditor);
}
TER
ConfidentialMPTConvert::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const amtToConvert = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
if (ctx_.tx.isFieldPresent(sfHolderElGamalPublicKey))
(*sleMptoken)[sfHolderElGamalPublicKey] = ctx_.tx[sfHolderElGamalPublicKey];
// Converting decreases regular balance and increases confidential outstanding.
// The confidential outstanding tracks total tokens in confidential form globally.
(*sleMptoken)[sfMPTAmount] = amt - amtToConvert;
(*sleIssuance)[sfConfidentialOutstandingAmount] =
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) + amtToConvert;
Slice const holderEc = ctx_.tx[sfHolderEncryptedAmount];
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
// Two cases for Convert:
// 1. Holder already has confidential balances -> homomorphically add to inbox
// 2. First-time convert -> initialize all confidential balance fields
if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
{
// Case 1: Add to existing inbox balance (holder will merge later)
{
Buffer sum(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(holderEc, (*sleMptoken)[sfConfidentialBalanceInbox], sum);
!isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*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; // LCOV_EXCL_LINE
(*sleMptoken)[sfIssuerEncryptedBalance] = sum;
}
// homomorphically add auditor's encrypted balance
if (auditorEc)
{
Buffer sum(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance], sum);
!isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfAuditorEncryptedBalance] = sum;
}
}
else if (
!sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
{
// Case 2: First-time convert - initialize all confidential fields
(*sleMptoken)[sfConfidentialBalanceInbox] = holderEc;
(*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc;
(*sleMptoken)[sfConfidentialBalanceVersion] = 0;
if (auditorEc)
(*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc;
// Spending balance starts at zero. Must use canonical zero encryption
// (deterministic ciphertext) so the ledger state is reproducible.
auto const zeroBalance =
encryptCanonicalZeroAmount((*sleMptoken)[sfHolderElGamalPublicKey], account_, mptIssuanceID);
if (!zeroBalance)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceSpending] = *zeroBalance;
}
else
{
// both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
// exist together
return tecINTERNAL; // LCOV_EXCL_LINE
}
view().update(sleIssuance);
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,44 @@
#pragma once
#include <xrpld/app/tx/detail/Transactor.h>
namespace xrpl {
/**
* @brief Converts public (plaintext) MPT balance to confidential (encrypted)
* balance.
*
* This transaction allows a token holder to convert their publicly visible
* MPToken balance into an encrypted confidential balance. Once converted,
* the balance can only be spent using ConfidentialMPTSend transactions and
* remains hidden from public view on the ledger.
*
* @par Cryptographic Operations:
* - **Schnorr Proof Verification**: When registering a new ElGamal public key,
* verifies proof of knowledge of the corresponding private key.
* - **Revealed Amount Verification**: Verifies that the provided encrypted
* amounts (for holder, issuer, and optionally auditor) all encrypt the
* same plaintext amount using the provided blinding factor.
*
* @see ConfidentialMPTConvertBack, ConfidentialMPTSend
*/
class ConfidentialMPTConvert : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTConvert(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,288 @@
#include <xrpld/app/tx/detail/ConfidentialMPTConvertBack.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>
#include <cstddef>
namespace xrpl {
NotTEC
ConfidentialMPTConvertBack::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot convert back
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;
if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > maxMPTokenAmount)
return temBAD_AMOUNT;
if (ctx.tx[sfBlindingFactor].size() != ecBlindingFactorLength)
return temMALFORMED;
if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]))
return temMALFORMED;
// check encrypted amount format after the above basic checks
// this check is more expensive so put it at the end
if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
return res;
// ConvertBack proof = pedersen linkage proof + single bulletproof
if (ctx.tx[sfZKProof].size() != ecPedersenProofLength + ecSingleBulletproofLength)
return temMALFORMED;
return tesSUCCESS;
}
/**
* Verifies the cryptographic proofs for a ConvertBack transaction.
*
* This function verifies three proofs:
* 1. Revealed amount proof: verifies the encrypted amounts (holder, issuer,
* auditor) all encrypt the same revealed amount using the blinding factor.
* 2. Pedersen linkage proof: verifies the balance commitment is derived from
* the holder's encrypted spending balance.
* 3. Bulletproof (range proof): verifies the remaining balance (balance - amount)
* is non-negative, preventing overdrafts.
*
* All proofs are verified before returning any error to prevent timing attacks.
*/
static TER
verifyProofs(STTx const& tx, std::shared_ptr<SLE const> const& issuance, std::shared_ptr<SLE const> const& mptoken)
{
if (!mptoken->isFieldPresent(sfHolderElGamalPublicKey))
return tecINTERNAL; // LCOV_EXCL_LINE
auto const mptIssuanceID = tx[sfMPTokenIssuanceID];
auto const account = tx[sfAccount];
auto const amount = tx[sfMPTAmount];
auto const blindingFactor = tx[sfBlindingFactor];
auto const holderPubKey = (*mptoken)[sfHolderElGamalPublicKey];
auto const contextHash = getConvertBackContextHash(
account, tx[sfSequence], mptIssuanceID, amount, (*mptoken)[~sfConfidentialBalanceVersion].value_or(0));
// Prepare Auditor Info
std::optional<ConfidentialRecipient> auditor;
bool const hasAuditor = issuance->isFieldPresent(sfAuditorElGamalPublicKey);
if (hasAuditor)
{
auditor.emplace(ConfidentialRecipient{(*issuance)[sfAuditorElGamalPublicKey], tx[sfAuditorEncryptedAmount]});
}
// Run all verifications before returning any error to prevent timing attacks
// that could reveal which proof failed.
bool valid = true;
// verify revealed amount
if (auto const ter = verifyRevealedAmount(
amount,
blindingFactor,
{holderPubKey, tx[sfHolderEncryptedAmount]},
{(*issuance)[sfIssuerElGamalPublicKey], tx[sfIssuerEncryptedAmount]},
auditor);
!isTesSuccess(ter))
{
valid = false;
}
// Parse proof components using offset
auto const proof = tx[sfZKProof];
size_t remainingLength = proof.size();
size_t currentOffset = 0;
// Extract Pedersen linkage proof
if (remainingLength < ecPedersenProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const pedersenProof = proof.substr(currentOffset, ecPedersenProofLength);
currentOffset += ecPedersenProofLength;
remainingLength -= ecPedersenProofLength;
// Extract bulletproof
if (remainingLength < ecSingleBulletproofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const bulletproof = proof.substr(currentOffset, ecSingleBulletproofLength);
currentOffset += ecSingleBulletproofLength;
remainingLength -= ecSingleBulletproofLength;
if (remainingLength != 0)
return tecINTERNAL; // LCOV_EXCL_LINE
// verify el gamal pedersen linkage
if (auto const ter = verifyBalancePcmLinkage(
pedersenProof,
(*mptoken)[sfConfidentialBalanceSpending],
holderPubKey,
tx[sfBalanceCommitment],
contextHash);
!isTesSuccess(ter))
{
valid = false;
}
// verify bullet proof
{
// Compute PC_rem = PC_balance - mG (the commitment to the remaining balance)
Buffer pcRem;
if (auto const ter = computeConvertBackRemainder(tx[sfBalanceCommitment], amount, pcRem); !isTesSuccess(ter))
{
valid = false;
}
// The bulletproof verifies that the remaining balance is non-negative
std::vector<Slice> commitments{Slice(pcRem.data(), pcRem.size())};
if (auto const ter = verifyAggregatedBulletproof(bulletproof, commitments, contextHash); !isTesSuccess(ter))
{
valid = false;
}
}
if (!valid)
return tecBAD_PROOF;
return tesSUCCESS;
}
TER
ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx)
{
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const account = ctx.tx[sfAccount];
auto const amount = ctx.tx[sfMPTAmount];
// ensure that issuance exists
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
return tecNO_PERMISSION;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey);
// tx must include auditor ciphertext if the issuance has enabled
// auditing
if (requiresAuditor && !hasAuditor)
return tecNO_PERMISSION;
// if auditing is not supported then user should not upload auditor
// ciphertext
if (!requiresAuditor && hasAuditor)
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) == account)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
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) < amount)
{
return tecINSUFFICIENT_FUNDS;
}
// 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;
if (TER const res = verifyProofs(ctx.tx, sleIssuance, sleMptoken); !isTesSuccess(res))
return res;
return tesSUCCESS;
}
TER
ConfidentialMPTConvertBack::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const amtToConvertBack = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
// Converting back increases regular balance and decreases confidential
// outstanding. This is the inverse of Convert.
(*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack;
(*sleIssuance)[sfConfidentialOutstandingAmount] =
(*sleIssuance)[sfConfidentialOutstandingAmount] - amtToConvertBack;
std::optional<Slice> const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
// homomorphically subtract holder's encrypted balance
{
Buffer res(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicSubtract(
(*sleMptoken)[sfConfidentialBalanceSpending], ctx_.tx[sfHolderEncryptedAmount], res);
!isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*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; // LCOV_EXCL_LINE
(*sleMptoken)[sfIssuerEncryptedBalance] = res;
}
if (auditorEc)
{
Buffer res(ecGamalEncryptedTotalLength);
if (TER const ter =
homomorphicSubtract((*sleMptoken)[sfAuditorEncryptedBalance], ctx_.tx[sfAuditorEncryptedAmount], res);
!isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfAuditorEncryptedBalance] = res;
}
incrementConfidentialVersion(*sleMptoken);
view().update(sleIssuance);
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,45 @@
#pragma once
#include <xrpld/app/tx/detail/Transactor.h>
namespace xrpl {
/**
* @brief Converts confidential (encrypted) MPT balance back to public
* (plaintext) balance.
*
* This transaction allows a token holder to convert their encrypted
* confidential balance back into a publicly visible MPToken balance. The
* holder must prove they have sufficient confidential balance without
* revealing the actual balance amount.
*
* @par Cryptographic Operations:
* - **Revealed Amount Verification**: Verifies that the provided encrypted
* amounts correctly encrypt the conversion amount.
* - **Pedersen Linkage Proof**: Verifies that the provided balance commitment
* correctly links to the holder's encrypted spending balance.
* - **Bulletproof Range Proof**: Verifies that the remaining balance (after
* conversion) is non-negative, ensuring the holder has sufficient funds.
*
* @see ConfidentialMPTConvert, ConfidentialMPTSend
*/
class ConfidentialMPTConvertBack : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTConvertBack(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,95 @@
#include <xrpld/app/tx/detail/ConfidentialMPTMergeInbox.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 xrpl {
NotTEC
ConfidentialMPTMergeInbox::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
ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx)
{
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleMptoken = ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfHolderElGamalPublicKey))
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
ConfidentialMPTMergeInbox::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
// sanity check
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleMptoken->isFieldPresent(sfHolderElGamalPublicKey))
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Merge inbox into spending: spending = spending + inbox
// This allows holder to use received funds. Without merging, incoming
// transfers sit in inbox and cannot be spent or converted back.
Buffer sum(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(
(*sleMptoken)[sfConfidentialBalanceSpending], (*sleMptoken)[sfConfidentialBalanceInbox], sum);
!isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceSpending] = sum;
// Reset inbox to encrypted zero. Must use canonical zero encryption
// (deterministic ciphertext) so the ledger state is reproducible.
auto const zeroEncryption =
encryptCanonicalZeroAmount((*sleMptoken)[sfHolderElGamalPublicKey], account_, mptIssuanceID);
if (!zeroEncryption)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceInbox] = *zeroEncryption;
incrementConfidentialVersion(*sleMptoken);
view().update(sleMptoken);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,46 @@
#pragma once
#include <xrpld/app/tx/detail/Transactor.h>
namespace xrpl {
/**
* @brief Merges the confidential inbox balance into the spending balance.
*
* In the confidential transfer system, incoming funds are deposited into an
* "inbox" balance that the recipient cannot immediately spend. This prevents
* front-running attacks where an attacker could invalidate a pending
* transaction by sending funds to the sender. This transaction merges the
* inbox into the spending balance, making those funds available for spending.
*
* @par Cryptographic Operations:
* - **Homomorphic Addition**: Adds the encrypted inbox balance to the
* encrypted spending balance using ElGamal homomorphic properties.
* - **Zero Encryption**: Resets the inbox to an encryption of zero.
*
* @note This transaction requires no zero-knowledge proofs because it only
* combines encrypted values that the holder already owns. The
* homomorphic properties of ElGamal encryption ensure correctness.
*
* @see ConfidentialMPTSend, ConfidentialMPTConvert
*/
class ConfidentialMPTMergeInbox : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTMergeInbox(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,397 @@
#include <xrpld/app/tx/detail/ConfidentialMPTSend.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 xrpl {
NotTEC
ConfidentialMPTSend::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();
// ConfidentialMPTSend 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;
// Check the length of the encrypted amounts
if (ctx.tx[sfSenderEncryptedAmount].length() != ecGamalEncryptedTotalLength ||
ctx.tx[sfDestinationEncryptedAmount].length() != ecGamalEncryptedTotalLength ||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
if (hasAuditor && ctx.tx[sfAuditorEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
// Check the length of the ZKProof
auto const recipientCount = getConfidentialRecipientCount(hasAuditor);
auto const sizeEquality = getMultiCiphertextEqualityProofSize(recipientCount);
auto const sizePedersenLinkage = 2 * ecPedersenProofLength;
if (ctx.tx[sfZKProof].length() != sizeEquality + sizePedersenLinkage + ecDoubleBulletproofLength)
return temMALFORMED;
// Check the Pedersen commitments are valid
if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]) || !isValidCompressedECPoint(ctx.tx[sfAmountCommitment]))
return temMALFORMED;
// Check the encrypted amount formats, this is more expensive so put it at
// the end
if (!isValidCiphertext(ctx.tx[sfSenderEncryptedAmount]) ||
!isValidCiphertext(ctx.tx[sfDestinationEncryptedAmount]) || !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
return temBAD_CIPHERTEXT;
if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount]))
return temBAD_CIPHERTEXT;
return tesSUCCESS;
}
TER
verifySendProofs(
PreclaimContext const& ctx,
std::shared_ptr<SLE const> const& sleSenderMPToken,
std::shared_ptr<SLE const> const& sleDestinationMPToken,
std::shared_ptr<SLE const> const& sleIssuance)
{
// Sanity check
if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
auto const recipientCount = getConfidentialRecipientCount(hasAuditor);
auto const proof = ctx.tx[sfZKProof];
size_t remainingLength = proof.size();
size_t currentOffset = 0;
// Extract equality proof
auto const sizeEquality = getMultiCiphertextEqualityProofSize(recipientCount);
if (remainingLength < sizeEquality)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const equalityProof = proof.substr(currentOffset, sizeEquality);
currentOffset += sizeEquality;
remainingLength -= sizeEquality;
// Extract Pedersen linkage proof for amount commitment
if (remainingLength < ecPedersenProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const amountLinkageProof = proof.substr(currentOffset, ecPedersenProofLength);
currentOffset += ecPedersenProofLength;
remainingLength -= ecPedersenProofLength;
// Extract Pedersen linkage proof for balance commitment
if (remainingLength < ecPedersenProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const balanceLinkageProof = proof.substr(currentOffset, ecPedersenProofLength);
currentOffset += ecPedersenProofLength;
remainingLength -= ecPedersenProofLength;
// Extract range proof
if (remainingLength < ecDoubleBulletproofLength)
return tecINTERNAL;
auto const rangeProof = proof.substr(currentOffset, ecDoubleBulletproofLength);
currentOffset += ecDoubleBulletproofLength;
remainingLength -= ecDoubleBulletproofLength;
if (remainingLength != 0)
return tecINTERNAL; // LCOV_EXCL_LINE
// Prepare recipient list
std::vector<ConfidentialRecipient> recipients;
recipients.reserve(recipientCount);
recipients.push_back({(*sleSenderMPToken)[sfHolderElGamalPublicKey], ctx.tx[sfSenderEncryptedAmount]});
recipients.push_back({(*sleDestinationMPToken)[sfHolderElGamalPublicKey], ctx.tx[sfDestinationEncryptedAmount]});
recipients.push_back({(*sleIssuance)[sfIssuerElGamalPublicKey], ctx.tx[sfIssuerEncryptedAmount]});
if (hasAuditor)
{
recipients.push_back({(*sleIssuance)[sfAuditorElGamalPublicKey], ctx.tx[sfAuditorEncryptedAmount]});
}
// Prepare the context hash
auto const contextHash = getSendContextHash(
ctx.tx[sfAccount],
ctx.tx[sfSequence],
ctx.tx[sfMPTokenIssuanceID],
ctx.tx[sfDestination],
(*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0));
// Use a boolean flag to track validity instead of returning early on failure to prevent leaking information about
// which proof failed through timing differences
bool valid = true;
// Verify the multi-ciphertext equality proof
if (auto const ter = verifyMultiCiphertextEqualityProof(equalityProof, recipients, recipientCount, contextHash);
!isTesSuccess(ter))
{
valid = false;
}
// Verify amount linkage
if (auto const ter = verifyAmountPcmLinkage(
amountLinkageProof,
ctx.tx[sfSenderEncryptedAmount],
(*sleSenderMPToken)[sfHolderElGamalPublicKey],
ctx.tx[sfAmountCommitment],
contextHash);
!isTesSuccess(ter))
{
valid = false;
}
// Verify balance linkage
if (auto const ter = verifyBalancePcmLinkage(
balanceLinkageProof,
(*sleSenderMPToken)[sfConfidentialBalanceSpending],
(*sleSenderMPToken)[sfHolderElGamalPublicKey],
ctx.tx[sfBalanceCommitment],
contextHash);
!isTesSuccess(ter))
{
valid = false;
}
// Verify Range Proof
{
Buffer pcRem;
// Derive PC_rem = PC_balance - PC_amount
if (auto const ter = computeSendRemainder(ctx.tx[sfBalanceCommitment], ctx.tx[sfAmountCommitment], pcRem);
!isTesSuccess(ter))
{
valid = false;
}
else
{
// Aggregated commitments: [PC_amount, PC_rem]
// Prove that both the transfer amount and the remaining balance are in range
std::vector<Slice> commitments;
commitments.push_back(ctx.tx[sfAmountCommitment]);
commitments.push_back(Slice{pcRem.data(), pcRem.size()});
if (auto const ter = verifyAggregatedBulletproof(rangeProof, commitments, contextHash); !isTesSuccess(ter))
{
valid = false;
}
}
}
if (!valid)
{
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: One or more cryptographic proofs failed.";
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
{
// Check if sender account exists
auto const account = ctx.tx[sfAccount];
if (!ctx.view.exists(keylet::account(account)))
return terNO_ACCOUNT;
// Check if destination account exists
auto const destination = ctx.tx[sfDestination];
if (!ctx.view.exists(keylet::account(destination)))
return tecNO_TARGET;
// Check if MPT issuance exists
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// Check if the issuance allows transfer
if (!sleIssuance->isFlag(lsfMPTCanTransfer))
return tecNO_AUTH;
// Check if issuance allows confidential transfer
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
return tecNO_PERMISSION;
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey);
// Tx must include auditor ciphertext if the issuance has enabled
// auditing, and must not include it if auditing is not enabled
if (requiresAuditor != hasAuditor)
return tecNO_PERMISSION;
// Sanity check: issuer isn't the sender
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
// Check sender's MPToken existence
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;
// Sanity check: MPToken's auditor field must be present if auditing is
// enabled
if (requiresAuditor && !sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance))
return tefINTERNAL;
// Check destination's MPToken existence
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;
return verifySendProofs(ctx, sleSenderMPToken, sleDestinationMPToken, sleIssuance);
}
TER
ConfidentialMPTSend::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto const destination = ctx_.tx[sfDestination];
auto sleSenderMPToken = view().peek(keylet::mptoken(mptIssuanceID, account_));
auto sleDestinationMPToken = view().peek(keylet::mptoken(mptIssuanceID, destination));
auto sleDestAcct = view().peek(keylet::account(destination));
if (!sleSenderMPToken || !sleDestinationMPToken || !sleDestAcct)
return tecINTERNAL; // LCOV_EXCL_LINE
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];
auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
// Subtract from sender's spending balance
{
Slice const curSpending = (*sleSenderMPToken)[sfConfidentialBalanceSpending];
Buffer newSpending(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicSubtract(curSpending, senderEc, newSpending); !isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleSenderMPToken)[sfConfidentialBalanceSpending] = newSpending;
}
// Subtract from issuer's balance
{
Slice const curIssuerEnc = (*sleSenderMPToken)[sfIssuerEncryptedBalance];
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicSubtract(curIssuerEnc, issuerEc, newIssuerEnc); !isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleSenderMPToken)[sfIssuerEncryptedBalance] = newIssuerEnc;
}
// Subtract from auditor's balance if present
if (auditorEc)
{
Slice const curAuditorEnc = (*sleSenderMPToken)[sfAuditorEncryptedBalance];
Buffer newAuditorEnc(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicSubtract(curAuditorEnc, *auditorEc, newAuditorEnc); !isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleSenderMPToken)[sfAuditorEncryptedBalance] = newAuditorEnc;
}
// Add to destination's inbox balance
{
Slice const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox];
Buffer newInbox(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(curInbox, destEc, newInbox); !isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleDestinationMPToken)[sfConfidentialBalanceInbox] = newInbox;
}
// Add to issuer's balance
{
Slice const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance];
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(curIssuerEnc, issuerEc, newIssuerEnc); !isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleDestinationMPToken)[sfIssuerEncryptedBalance] = newIssuerEnc;
}
// Add to auditor's balance if present
if (auditorEc)
{
Slice const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance];
Buffer newAuditorEnc(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(curAuditorEnc, *auditorEc, newAuditorEnc); !isTesSuccess(ter))
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleDestinationMPToken)[sfAuditorEncryptedBalance] = newAuditorEnc;
}
// increment version
incrementConfidentialVersion(*sleSenderMPToken);
incrementConfidentialVersion(*sleDestinationMPToken);
view().update(sleSenderMPToken);
view().update(sleDestinationMPToken);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,52 @@
#pragma once
#include <xrpld/app/tx/detail/Transactor.h>
namespace xrpl {
/**
* @brief Transfers confidential MPT tokens between holders privately.
*
* This transaction enables private token transfers where the transfer amount
* is hidden from public view. Both sender and recipient must have initialized
* confidential balances. The transaction provides encrypted amounts for all
* parties (sender, destination, issuer, and optionally auditor) along with
* zero-knowledge proofs that verify correctness without revealing the amount.
*
* @par Cryptographic Operations:
* - **Multi-Ciphertext Equality Proof**: Verifies that all encrypted amounts
* (sender, destination, issuer, auditor) encrypt the same plaintext value.
* - **Amount Pedersen Linkage Proof**: Verifies that the amount commitment
* correctly links to the sender's encrypted amount.
* - **Balance Pedersen Linkage Proof**: Verifies that the balance commitment
* correctly links to the sender's encrypted spending balance.
* - **Bulletproof Range Proof**: Verifies remaining balance and
* transfer amount are non-negative.
*
* @note Funds are deposited into the destination's inbox, not spending
* balance. The recipient must call ConfidentialMPTMergeInbox to make
* received funds spendable.
*
* @see ConfidentialMPTMergeInbox, ConfidentialMPTConvert,
* ConfidentialMPTConvertBack
*/
class ConfidentialMPTSend : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialMPTSend(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -3310,4 +3310,204 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
return true;
}
void
ValidConfidentialMPToken::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
// Helper to get MPToken Issuance ID safely
auto const getMptID = [](std::shared_ptr<SLE const> const& sle) -> uint192 {
if (!sle)
return beast::zero;
if (sle->getType() == ltMPTOKEN)
return sle->getFieldH192(sfMPTokenIssuanceID);
if (sle->getType() == ltMPTOKEN_ISSUANCE)
return makeMptID(sle->getFieldU32(sfSequence), sle->getAccountID(sfIssuer));
return beast::zero;
};
if (before && before->getType() == ltMPTOKEN)
{
uint192 const id = getMptID(before);
changes_[id].mptAmountDelta -= before->getFieldU64(sfMPTAmount);
// Cannot delete MPToken with non-zero confidential state or non-zero public amount
if (isDelete)
{
bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0;
bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) ||
before->isFieldPresent(sfConfidentialBalanceInbox) || before->isFieldPresent(sfIssuerEncryptedBalance);
if (hasPublicBalance || hasEncryptedFields)
changes_[id].deletedWithEncrypted = true;
}
}
if (after && after->getType() == ltMPTOKEN)
{
uint192 const id = getMptID(after);
changes_[id].mptAmountDelta += after->getFieldU64(sfMPTAmount);
// Encrypted field existence consistency
bool const hasIssuerBalance = after->isFieldPresent(sfIssuerEncryptedBalance);
bool const hasHolderInbox = after->isFieldPresent(sfConfidentialBalanceInbox);
bool const hasHolderSpending = after->isFieldPresent(sfConfidentialBalanceSpending);
bool const hasAnyHolder = hasHolderInbox || hasHolderSpending;
if (hasAnyHolder != hasIssuerBalance)
{
changes_[id].badConsistency = true;
}
// Privacy flag consistency
bool const hasEncrypted = hasAnyHolder || hasIssuerBalance;
if (hasEncrypted)
changes_[id].requiresPrivacyFlag = true;
}
if (before && before->getType() == ltMPTOKEN_ISSUANCE)
{
uint192 const id = getMptID(before);
if (before->isFieldPresent(sfConfidentialOutstandingAmount))
changes_[id].coaDelta -= before->getFieldU64(sfConfidentialOutstandingAmount);
changes_[id].outstandingDelta -= before->getFieldU64(sfOutstandingAmount);
}
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
{
uint192 const id = getMptID(after);
auto& change = changes_[id];
bool const hasCOA = after->isFieldPresent(sfConfidentialOutstandingAmount);
std::uint64_t const coa = (*after)[~sfConfidentialOutstandingAmount].value_or(0);
std::uint64_t const oa = after->getFieldU64(sfOutstandingAmount);
if (hasCOA)
change.coaDelta += coa;
change.outstandingDelta += oa;
change.issuance = after;
// COA <= OutstandingAmount
if (coa > oa)
change.badCOA = true;
}
if (before && after && before->getType() == ltMPTOKEN && after->getType() == ltMPTOKEN)
{
uint192 const id = getMptID(after);
// sfConfidentialBalanceVersion must change when spending changes
auto const spendingBefore = (*before)[~sfConfidentialBalanceSpending];
auto const spendingAfter = (*after)[~sfConfidentialBalanceSpending];
auto const versionBefore = (*before)[~sfConfidentialBalanceVersion];
auto const versionAfter = (*after)[~sfConfidentialBalanceVersion];
if (spendingBefore.has_value() && spendingBefore != spendingAfter)
{
if (versionBefore == versionAfter)
{
changes_[id].badVersion = true;
}
}
}
}
bool
ValidConfidentialMPToken::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (result != tesSUCCESS)
return true;
for (auto const& [id, checks] : changes_)
{
// Find the MPTokenIssuance
auto const issuance = [&]() -> std::shared_ptr<SLE const> {
if (checks.issuance)
return checks.issuance;
return view.read(keylet::mptIssuance(id));
}();
// Skip all invariance checks if issuance doesn't exist because that means the MPT has been deleted
if (!issuance)
continue;
// Cannot delete MPToken with non-zero confidential state
if (checks.deletedWithEncrypted)
{
if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPToken deleted with encrypted fields while COA > 0";
return false;
}
}
// Encrypted field existence consistency
if (checks.badConsistency)
{
JLOG(j.fatal()) << "Invariant failed: MPToken encrypted field "
"existence inconsistency";
return false;
}
// COA <= OutstandingAmount
if (checks.badCOA)
{
JLOG(j.fatal()) << "Invariant failed: Confidential outstanding amount "
"exceeds total outstanding amount";
return false;
}
// Privacy flag consistency
if (checks.requiresPrivacyFlag)
{
if (!issuance->isFlag(lsfMPTCanPrivacy))
{
JLOG(j.fatal()) << "Invariant failed: MPToken has encrypted "
"fields but Issuance does not have "
"lsfMPTCanPrivacy set";
return false;
}
}
// We only enforce this when Confidential Outstanding Amount changes (Convert, ConvertBack,
// ConfidentialClawback). This avoids falsely failing on Escrow or AMM operations that lock public tokens
// outside of ltMPTOKEN.
// Convert / ConvertBack:
// - COA and MPTAmount must have opposite deltas, which cancel each other out to zero.
// - OA remains unchanged.
// - Therefore, the net delta on both sides of the equation is zero.
//
// Clawback:
// - MPTAmount remains unchanged.
// - COA and OA must have identical deltas (mirrored on each side).
// - The equation remains balanced as both sides have equal offsets.
if (checks.coaDelta != 0)
{
if (checks.mptAmountDelta + checks.coaDelta != checks.outstandingDelta)
{
JLOG(j.fatal()) << "Invariant failed: Token conservation "
"violation for MPT "
<< to_string(id);
return false;
}
}
if (checks.badVersion)
{
JLOG(j.fatal()) << "Invariant failed: MPToken sfConfidentialBalanceVersion not updated when "
"sfConfidentialBalanceSpending changed";
return false;
}
}
return true;
}
} // namespace xrpl

View File

@@ -678,6 +678,48 @@ public:
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Confidential MPToken consistency
*
* - Convert/ConvertBack symmetry:
* Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
* - Cannot delete MPToken with non-zero confidential state:
* Cannot delete if sfIssuerEncryptedBalance exists
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
* - Privacy flag consistency:
* MPToken can only have encrypted fields if lsfMPTCanPrivacy is set on
* issuance.
* - Encrypted field existence consistency:
* If sfConfidentialBalanceSpending/sfConfidentialBalanceInbox exists, then
* sfIssuerEncryptedBalance must also exist (and vice versa).
* - COA <= OutstandingAmount:
* Confidential outstanding balance cannot exceed total outstanding.
* - Verifies sfConfidentialBalanceVersion is changed whenever sfConfidentialBalanceSpending is modified on an MPToken.
*/
class ValidConfidentialMPToken
{
struct Changes
{
std::int64_t mptAmountDelta = 0;
std::int64_t coaDelta = 0;
std::int64_t outstandingDelta = 0;
SLE::const_pointer issuance;
bool deletedWithEncrypted = false;
bool badConsistency = false;
bool badCOA = false;
bool requiresPrivacyFlag = false;
bool badVersion = false;
};
std::map<uint192, Changes> changes_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
// additional invariant checks can be declared above and then added to this
// tuple
using InvariantChecks = std::tuple<
@@ -704,7 +746,8 @@ using InvariantChecks = std::tuple<
ValidPseudoAccounts,
ValidLoanBroker,
ValidLoan,
ValidVault>;
ValidVault,
ValidConfidentialMPToken>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -70,6 +70,23 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
if (ctx.view.rules().enabled(featureSingleAssetVault) && 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

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

View File

@@ -1,6 +1,7 @@
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/MPTokenIssuanceSet.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TxFlags.h>
@@ -27,16 +28,19 @@ struct MPTMutabilityFlags
{
std::uint32_t setFlag;
std::uint32_t clearFlag;
std::uint32_t canMutateFlag;
std::uint32_t mutabilityFlag;
std::uint32_t targetFlag;
bool isCannotMutate = false; // if true, cannot mutate by default.
};
static constexpr std::array<MPTMutabilityFlags, 6> mptMutabilityFlags = {
{{tmfMPTSetCanLock, tmfMPTClearCanLock, lsmfMPTCanMutateCanLock},
{tmfMPTSetRequireAuth, tmfMPTClearRequireAuth, lsmfMPTCanMutateRequireAuth},
{tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, lsmfMPTCanMutateCanEscrow},
{tmfMPTSetCanTrade, tmfMPTClearCanTrade, lsmfMPTCanMutateCanTrade},
{tmfMPTSetCanTransfer, tmfMPTClearCanTransfer, lsmfMPTCanMutateCanTransfer},
{tmfMPTSetCanClawback, tmfMPTClearCanClawback, lsmfMPTCanMutateCanClawback}}};
static constexpr std::array<MPTMutabilityFlags, 7> mptMutabilityFlags = {
{{tmfMPTSetCanLock, tmfMPTClearCanLock, lsmfMPTCanMutateCanLock, lsfMPTCanLock},
{tmfMPTSetRequireAuth, tmfMPTClearRequireAuth, lsmfMPTCanMutateRequireAuth, lsfMPTRequireAuth},
{tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, lsmfMPTCanMutateCanEscrow, lsfMPTCanEscrow},
{tmfMPTSetCanTrade, tmfMPTClearCanTrade, lsmfMPTCanMutateCanTrade, lsfMPTCanTrade},
{tmfMPTSetCanTransfer, tmfMPTClearCanTransfer, lsmfMPTCanMutateCanTransfer, lsfMPTCanTransfer},
{tmfMPTSetCanClawback, tmfMPTClearCanClawback, lsmfMPTCanMutateCanClawback, lsfMPTCanClawback},
{tmfMPTSetPrivacy, tmfMPTClearPrivacy, lsmfMPTCannotMutatePrivacy, lsfMPTCanPrivacy, true}}};
NotTEC
MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
@@ -45,14 +49,27 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
auto const metadata = ctx.tx[~sfMPTokenMetadata];
auto const transferFee = ctx.tx[~sfTransferFee];
auto const isMutate = mutableFlags || metadata || transferFee;
auto const hasIssuerElGamalKey = ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey);
auto const hasAuditorElGamalKey = ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey);
auto const txFlags = ctx.tx.getFlags();
auto const mutatePrivacy = mutableFlags && ((*mutableFlags & (tmfMPTSetPrivacy | tmfMPTClearPrivacy)));
auto const hasDomain = ctx.tx.isFieldPresent(sfDomainID);
auto const hasHolder = ctx.tx.isFieldPresent(sfHolder);
if (isMutate && !ctx.rules.enabled(featureDynamicMPT))
return temDISABLED;
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
if ((hasIssuerElGamalKey || hasAuditorElGamalKey || mutatePrivacy) &&
!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
if (hasDomain && hasHolder)
return temMALFORMED;
auto const txFlags = ctx.tx.getFlags();
if (mutatePrivacy && hasHolder)
return temMALFORMED;
// fails if both flags are set
if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock))
@@ -63,10 +80,11 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (holderID && accountID == holderID)
return temMALFORMED;
if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT))
if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT) ||
ctx.rules.enabled(featureConfidentialTransfer))
{
// Is this transaction actually changing anything ?
if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate)
if (txFlags == 0 && !hasDomain && !hasIssuerElGamalKey && !hasAuditorElGamalKey && !isMutate)
return temMALFORMED;
}
@@ -104,6 +122,18 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
}
}
if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
return temMALFORMED;
if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
return temMALFORMED;
if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerElGamalPublicKey]))
return temMALFORMED;
if (hasAuditorElGamalKey && !isValidCompressedECPoint(ctx.tx[sfAuditorElGamalPublicKey]))
return temMALFORMED;
return tesSUCCESS;
}
@@ -193,13 +223,26 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
auto isMutableFlag = [&](std::uint32_t mutableFlag) -> bool { return currentMutableFlags & mutableFlag; };
if (auto const mutableFlags = ctx.tx[~sfMutableFlags])
auto const mutableFlags = ctx.tx[~sfMutableFlags];
if (mutableFlags)
{
if (std::any_of(
mptMutabilityFlags.begin(), mptMutabilityFlags.end(), [mutableFlags, &isMutableFlag](auto const& f) {
return !isMutableFlag(f.canMutateFlag) && ((*mutableFlags & (f.setFlag | f.clearFlag)));
bool const canMutate =
f.isCannotMutate ? isMutableFlag(f.mutabilityFlag) : !isMutableFlag(f.mutabilityFlag);
return canMutate && (*mutableFlags & (f.setFlag | f.clearFlag));
}))
return tecNO_PERMISSION;
if ((*mutableFlags & tmfMPTSetPrivacy) || (*mutableFlags & tmfMPTClearPrivacy))
{
std::uint64_t const confidentialOA = (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0);
// If there's any confidential outstanding amount, disallow toggling
// the lsfMPTCanPrivacy flag
if (confidentialOA > 0)
return tecNO_PERMISSION;
}
}
if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata))
@@ -218,6 +261,35 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
// cannot update issuer public key
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && sleMptIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
{
return tecNO_PERMISSION;
}
// cannot update auditor public key
if (ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey) && sleMptIssuance->isFieldPresent(sfAuditorElGamalPublicKey))
{
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && !sleMptIssuance->isFlag(lsfMPTCanPrivacy))
{
return tecNO_PERMISSION;
}
if (ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey) && !sleMptIssuance->isFlag(lsfMPTCanPrivacy))
{
return tecNO_PERMISSION;
}
// cannot upload key if there's circulating supply of COA
if ((ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) || ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey)) &&
sleMptIssuance->isFieldPresent(sfConfidentialOutstandingAmount))
{
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
@@ -251,9 +323,9 @@ MPTokenIssuanceSet::doApply()
for (auto const& f : mptMutabilityFlags)
{
if (mutableFlags & f.setFlag)
flagsOut |= f.canMutateFlag;
flagsOut |= f.targetFlag;
else if (mutableFlags & f.clearFlag)
flagsOut &= ~f.canMutateFlag;
flagsOut &= ~f.targetFlag;
}
if (mutableFlags & tmfMPTClearCanTransfer)
@@ -303,6 +375,22 @@ 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);
}
if (auto const pubKey = ctx_.tx[~sfAuditorElGamalPublicKey])
{
// This is enforced in preflight.
XRPL_ASSERT(sle->getType() == ltMPTOKEN_ISSUANCE, "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
sle->setFieldVL(sfAuditorElGamalPublicKey, *pubKey);
}
view().update(sle);
return tesSUCCESS;