Compare commits

...
Author SHA1 Message Date
Shawn Xie 030772f9d3 Merge pull request #7654 from shawnxie999/ct-latest-dev
merge with latest develop
2026-06-26 13:58:12 -04:00
Shawn Xie 0b92a77342 Merge remote-tracking branch 'upstream/develop' into ct-latest-dev 2026-06-26 13:55:48 -04:00
e28dcdb065 Fix flags (#7649)
Co-authored-by: Mayukha Vadari <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Timothy Banks <[email protected]>
Co-authored-by: Timothy Banks <[email protected]>
Co-authored-by: Ayaz Salikhov <[email protected]>
2026-06-26 10:47:46 -07:00
Peter Chen f96d50b487 add server logging (#7645) 2026-06-26 09:40:16 -07:00
Peter ChenandAyaz Salikhov 734811d1dd merge develop and update mpt-crypto lib version (#7642)
Co-authored-by: Ayaz Salikhov <[email protected]>
2026-06-25 23:04:54 -07:00
yinyiqian1 485adaa864 clang-tidy 2026-06-26 01:19:12 -04:00
yinyiqian1 bf823324ee adapt lib API change 2026-06-26 00:57:23 -04:00
yinyiqian1 638bec1c99 update mpt-crypto 2026-06-26 00:35:56 -04:00
yinyiqian1 c995621297 Merge remote-tracking branch 'origin/develop' into updatelib 2026-06-26 00:30:49 -04:00
Peter Chen 7e7dc991e3 fix review comments (#7636) 2026-06-25 16:14:39 -07:00
Shawn Xie ce4370dfc3 Merge branch 'develop' into ripple/confidential-transfer 2026-06-25 12:39:36 -04:00
yinyiqian1 b695d76c49 fix: re-randomize auditor (#7612) 2026-06-25 10:03:13 -04:00
Peter Chen c9fb4e906b fix: Address review comments (#7609) 2026-06-24 14:53:35 -04:00
Peter Chen c52d0a495e Merge pull request #7598 from PeterChen13579/MergeConfidentialTransferWithDevelop
Merge confidential transfer with develop and remove tmfMPTClearCanConfidentialAmount flag
2026-06-23 12:43:48 -07:00
Peter Chen 391edb03ee Merge remote-tracking branch 'upstream/develop' into MergeConfidentialTransferWithDevelop 2026-06-23 10:42:36 -07:00
Peter Chen 113c92f27e remove tmfMPTClearCanConfidentialAmount flag 2026-06-23 10:41:06 -07:00
Peter Chen d6e5aff363 fix ubsan 2026-06-22 17:49:48 -07:00
Peter Chen d87102f061 fix test 2026-06-22 13:56:07 -07:00
Peter Chen 18b2feea3a fix tests 2026-06-22 13:31:07 -07:00
Peter Chen 71851f9b21 Merge remote-tracking branch 'upstream/develop' into MergeConfidentialTransferWithDevelop 2026-06-22 13:06:21 -07:00
Peter Chen 4447bd219d fix: NOLINT Clang Tidy Confidential MPT (#7573) 2026-06-22 14:40:53 -04:00
Shawn Xie fdbde0a5bc Cleanup code review comments (#7536) 2026-06-17 14:25:43 -04:00
Peter Chen 54e0f58424 Address Confidential Transfer Feedback and uncomment tests (#7541) 2026-06-17 13:32:18 -04:00
yinyiqian1 31a3a0db62 Re-randomize after each send (#7535) 2026-06-16 21:11:42 -04:00
Peter Chen 729bc70ec8 Merge pull request #7540 from PeterChen13579/mergeDevelop
Merge latest develop into Confidential MPT
2026-06-15 10:32:57 -07:00
Peter Chen 59d630ab09 Merge remote-tracking branch 'upstream/develop' into mergeDevelop 2026-06-15 08:48:34 -07:00
Peter Chen 5ec499245b Merge pull request #7372 from PeterChen13579/add_more_confidential_Mpt_test
add missing tests to Confidential MPT
2026-06-11 15:08:01 -07:00
Peter Chen f79eac53bc Merge pull request #7419 from PeterChen13579/mergeLatestDevelop
Merge latest develop into Confidential MPT
2026-06-11 08:00:57 -07:00
Peter Chen 9854b3efc2 fix clang tidy 2026-06-10 13:51:45 -07:00
Peter Chen e39356a75b fix clang tidy 2026-06-10 13:27:28 -07:00
Peter Chen aca11d08ce fix clang tidy 2026-06-10 13:03:54 -07:00
Peter Chen f118070075 Merge remote-tracking branch 'upstream/develop' into mergeLatestDevelop 2026-06-10 11:43:47 -07:00
Peter Chen 1aa983aac9 fix naming 2026-06-10 11:41:56 -07:00
Peter Chen b1bcd41d7f fix conan lock 2026-06-10 11:31:16 -07:00
Peter Chen e36927d4db fix lock 2026-06-10 11:05:42 -07:00
Peter Chen ab730e791c update mpt-crypto and uncomment confidential transfer tests 2026-06-10 10:52:23 -07:00
Peter Chen 7b5f86c6ea Merge remote-tracking branch 'upstream/develop' into mergeLatestDevelop 2026-06-08 08:26:27 -07:00
Peter Chen ecef6b6002 codegen 2026-06-06 07:32:47 -07:00
Peter Chen 617c56c9f9 Merge latest develop into Confidential MPT 2026-06-05 22:02:44 -07:00
Peter Chen 72ff8f9824 Revert "Merge develop into confidential MPT" (#7416) 2026-06-05 12:18:13 -07:00
Peter Chen bf17bd6c58 Merge pull request #7384 from PeterChen13579/merge_develop_into_confidentialMPT
Merge develop into confidential MPT
2026-06-05 09:39:30 -07:00
Peter Chen e64247e542 fix clang tidy 2026-06-02 15:40:01 -04:00
Peter Chen b14078bff9 fix clang tidy 2026-06-02 14:08:16 -04:00
Peter Chen 7471bfb182 Merge develop into confidential MPT 2026-06-02 13:01:46 -04:00
Peter Chen dacd108657 Refactor confidential Test into 2 files (#7351) 2026-05-29 14:14:41 -04:00
Peter Chen 5869c23ecd Add specific checks for COA and OA (#7275) 2026-05-26 13:44:36 -04:00
yinyiqian1 ca7da3600f Add Batch + Delegation + Confidential tests (#7279) 2026-05-15 14:08:58 -04:00
Peter Chen bab0c48d01 add amm pseudo account test with confidential Transfer (#7276) 2026-05-15 10:26:43 -04:00
Peter Chen 71983a59b6 add public MPT txn after Clearing Confidential Flag (#7113) 2026-05-13 09:57:18 -04:00
yinyiqian1 70f03eba66 Update error code for transfer rate error (#7115) 2026-05-12 13:31:13 -04:00
yinyiqian1 d7158abb63 Enforce zero transfer rate for confidential MPT (#7106) 2026-05-11 11:35:44 -04:00
yinyiqian1 c5cfeefc44 refactor and check deposit preauth before ZK proof (#7085) 2026-05-07 11:13:11 -04:00
yinyiqian1 ed6de36cb4 Increase confidential transactions fee (#7063) 2026-05-06 17:15:39 -04:00
Shawn Xie b831b661ed Merge develop into confidential-transfer
Merge `develop` into `confidential-transfer`
2026-05-04 20:45:24 -04:00
Shawn Xie cd193bba31 exclude bugprone access 2026-05-04 19:40:27 -04:00
Shawn Xie 086d95aa2c exclude constness for ConfidentialTransfer test file 2026-05-04 18:03:51 -04:00
Shawn Xie 6960e3a234 exclude const clang tidy 2026-05-04 17:20:48 -04:00
Shawn Xie d7d9e54f92 fix const 2026-05-04 16:08:55 -04:00
Shawn Xie 541c4afeca clang tidy 2026-05-04 15:45:12 -04:00
Shawn Xie 64b5c3cf8f update delegable test cnt 2026-05-04 15:12:18 -04:00
Shawn Xie feb9782387 fix autogen 2026-05-04 14:53:46 -04:00
Shawn Xie aa55d7e360 Merge branch 'ripple/confidential-transfer' into ct-merge-dev-may-4 2026-05-04 14:39:54 -04:00
Shawn Xie f1de2738a3 Fix clang tidy and compile errors 2026-05-04 14:39:03 -04:00
yinyiqian1andPeter Chen cad129921d add IssuerEncryptedBalance check in ConvertBack (#7060)
Co-authored-by: Peter Chen <[email protected]>
2026-05-04 14:32:23 -04:00
Shawn Xie 5afa29cd39 Merge remote-tracking branch 'upstream/develop' into ct-merge-dev-may-4
# Conflicts:
#	include/xrpl/protocol/detail/features.macro
#	include/xrpl/protocol/detail/ledger_entries.macro
#	include/xrpl/protocol/detail/secp256k1.h
#	include/xrpl/protocol/detail/transactions.macro
#	src/libxrpl/ledger/helpers/TokenHelpers.cpp
#	src/libxrpl/protocol/PublicKey.cpp
#	src/libxrpl/tx/invariants/MPTInvariant.cpp
#	src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
#	src/test/app/Delegate_test.cpp
#	src/test/jtx/impl/mpt.cpp
#	src/test/jtx/mpt.h
2026-05-04 13:51:58 -04:00
Shawn Xie 9a62a1e471 fix: add credential amendment check and more refactoring (#7041) 2026-05-01 12:25:01 -04:00
Peter Chen cc58844d49 Merge pull request #7048 from PeterChen13579/add_defensive_checks
add defensive checks
2026-04-30 15:12:06 -04:00
Peter Chen 58337c61c5 Fix comments 2026-04-30 14:36:19 -04:00
Peter Chen d09352af59 add another check 2026-04-29 13:36:20 -04:00
Peter Chen 7fad75d2b2 add test 2026-04-29 12:10:23 -04:00
Peter Chen 5f1c3ba4eb add defensive checks 2026-04-29 11:28:56 -04:00
Shawn Xie 9bfd78e2fc Add ZKP and ciphertext malleability tests (#6999) 2026-04-28 11:25:40 -04:00
Shawn Xie 5a643b1a7f fix: assorted safety checks (#7030) 2026-04-27 14:36:54 -04:00
yinyiqian1 8457b91b10 Test inconsistent C1 for ConfidentialMPTSend (#7017) 2026-04-24 15:03:26 -04:00
yinyiqian1 4520de22ef Merge pull request #7002 from yinyiqian1/merge-ct2
Merge develop into confidential-transfer
2026-04-23 12:24:22 -04:00
yinyiqian1 b0386622ca fix clang-tidy 2026-04-23 11:38:05 -04:00
yinyiqian1 c53d6aca08 resolve conflicts 2026-04-23 01:05:20 -04:00
yinyiqian1 be8de20a0b Merge branch 'develop' into merge-ct2 2026-04-23 00:41:44 -04:00
yinyiqian1 235547f460 Revert "Merge develop into ConfidentialTransfer (#6987)" (#7001) 2026-04-22 23:57:01 -04:00
+5 1e73f3f3ff Merge develop into ConfidentialTransfer (#6987)
Signed-off-by: dependabot[bot] <[email protected]>
Signed-off-by: chuanshanjida <[email protected]>
Co-authored-by: Ed Hennis <[email protected]>
Co-authored-by: Bart <[email protected]>
Co-authored-by: Bart <[email protected]>
Co-authored-by: Zhiyuan Wang <[email protected]>
Co-authored-by: Alex Kremer <[email protected]>
Co-authored-by: Ayaz Salikhov <[email protected]>
Co-authored-by: Sergey Kuznetsov <[email protected]>
Co-authored-by: JCW <[email protected]>
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gregory Tsipenyuk <[email protected]>
Co-authored-by: chuanshanjida <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Vito Tumas <[email protected]>
2026-04-22 23:34:03 -04:00
Shawn Xie 2b65b740d0 Add destination tag to ConfidentialMPTSend (#6988) 2026-04-22 17:07:31 -04:00
yinyiqian1 f08121de35 Address coding style comments (#6966) 2026-04-20 13:31:09 -04:00
yinyiqian1 09778f2fec Support compact AND-composed sigma proof (#6859) 2026-04-16 17:51:31 -04:00
Shawn Xie 5229ff5a45 fix: fix pipeline failures (#6850) 2026-04-10 13:42:59 -04:00
Shawn Xie 6a8016f288 Merge develop into confidential-transfer 2026-04-09 11:54:29 -04:00
Shawn Xie 5d38603f90 fix compile and test failure 2026-04-09 11:22:31 -04:00
Shawn Xie 6f4fddc1ae Merge remote-tracking branch 'upstream/develop' into ct-merge-mpt-dex 2026-04-09 11:05:31 -04:00
Peter Chen 4ae17f193f Add JS fields required for Clio mpt_holder_api (#6810) 2026-04-09 10:49:10 -04:00
Peter Chen 36c805692a add batch tests (#6598) 2026-04-08 14:45:11 -04:00
yinyiqian1 5bd7ce1dfd Add tests for confidential delegation with tickets (#6808) 2026-04-08 13:20:59 -04:00
yinyiqian1 0d7bf6948e Add delegation tests for Confidential Transfers (#6729) 2026-04-07 17:45:26 -04:00
Peter Chen 58e38e05f9 add non-curve point tests (#6695) 2026-04-07 13:50:08 -04:00
Shawn Xie 697786afab Merge develop into confidential-transfer
- Resolved conflicts from autogen files and modularization
2026-04-06 15:45:06 -04:00
Shawn Xie 834cb72515 Update autogen files and delegable count 2026-04-06 15:00:07 -04:00
Shawn Xie 261e7d7769 Resolve conflicts 2026-04-06 12:11:45 -04:00
Shawn Xie 042b09563b Merge remote-tracking branch 'upstream/develop' into ct-merge 2026-04-06 11:57:06 -04:00
yinyiqian1 cc2d6cbff6 Integrate mpt_utility SDK for tests (#6705) 2026-04-02 12:04:05 -04:00
yinyiqian1 6f1232140b remove dead functions due to integration (#6683) 2026-03-30 16:37:55 -04:00
Peter Chen 13b3a243a3 add tests: encrypt under wrong public key (#6650) 2026-03-30 10:38:03 -04:00
yinyiqian1 2472ef7df7 Integrate mpt-crypto SDK lib for on-chain verification (#6679) 2026-03-27 16:26:37 -04:00
Shawn Xie b34ecc476a Add ProofReader and auth/lock and overflow edge case checks (#6651) 2026-03-26 13:16:51 -04:00
Shawn Xie 4c0e6012e3 Handle edge cases checks in flag setting and token deletion (#6596) 2026-03-24 11:41:27 -04:00
Peter Chen 3f749ecf76 tests: add ticket tests (#6576) 2026-03-20 10:52:18 -04:00
Shawn Xie a43cf94ff7 update sfBlindingFactor to type uint256 and refactor helper functions to return std::optional 2026-03-19 14:04:03 -04:00
Shawn Xie 9f4cf28aea Rename privacy flag and public key names (#6550)
Corresponding spec change:
https://github.com/XRPLF/XRPL-Standards/pull/501

### Field Renames (SFields)

| Before | After |
|--------|-------|
| `sfIssuerElGamalPublicKey` | `sfIssuerEncryptionKey` |
| `sfHolderElGamalPublicKey` | `sfHolderEncryptionKey` |
| `sfAuditorElGamalPublicKey` | `sfAuditorEncryptionKey` |

### Flag Renames

#### Transaction Flags (`tf`)

| Before | After |
|--------|-------|
| `tfMPTCanPrivacy` | `tfMPTCanConfidentialAmount` |

#### Ledger State Flags (`lsf`)

| Before | After |
|--------|-------|
| `lsfMPTCanPrivacy` | `lsfMPTCanConfidentialAmount` |

#### Ledger State Mutable Flags (`lsmf`)

| Before | After |
|--------|-------|
| `lsmfMPTCannotMutatePrivacy` |
`lsmfMPTCannotMutateCanConfidentialAmount` |

#### Transaction Mutable Flags (`tmf`)

| Before | After |
|--------|-------|
| `tmfMPTCannotMutatePrivacy` |
`tmfMPTCannotMutateCanConfidentialAmount` |
| `tmfMPTSetPrivacy` | `tmfMPTSetCanConfidentialAmount` |
| `tmfMPTClearPrivacy` | `tmfMPTClearCanConfidentialAmount` |
2026-03-17 10:53:17 -04:00
Peter Chen c45177b69e add homomorphic tests (#6490) 2026-03-16 11:27:36 -04:00
Shawn Xie d615098849 refactor: improve readability and clean up code from review comments (#6544) 2026-03-16 10:47:39 -04:00
Shawn Xie eeb0d15ea9 refactor: return optional buffers for helper functions (#6520) 2026-03-11 12:02:27 -04:00
Shawn Xie 84cc8599af chore: Apply clang-format width 100 (#6516) 2026-03-10 11:24:01 -04:00
Shawn Xie 36c1c5f3cd Merge develop into confidential-transfer 2026-03-09 16:50:01 -04:00
Shawn Xie 2c94e213c2 Resolve conflicts 2026-03-09 15:32:42 -04:00
Shawn Xie cbcc75ff2d Merge remote-tracking branch 'upstream/develop' into ct-merge-dev-100col 2026-03-09 15:25:10 -04:00
Shawn Xie da7698974c Merge commit '2c1fad102353e11293e3edde1c043224e7d3e983' into ct-merge-dev-100col 2026-03-09 14:24:26 -04:00
Shawn Xie a3a82faa3d fix conflicts and update modularization 2026-03-09 14:23:22 -04:00
Shawn Xie fff83c1d4a Merge commit '25cca465538a56cce501477f9e5e2c1c7ea2d84c' into ct-merge-dev-100col 2026-03-09 14:02:31 -04:00
Shawn Xie 803ab67fc5 fix: address auditor feedbacks (#6511) 2026-03-09 13:59:32 -04:00
yinyiqian1 734b11c0e3 Support shared r for ConfidentialMPTSend equality proof (#6496)
* Support shared r for send equality proof
2026-03-06 18:28:35 -05:00
Peter Chen fae34d0f36 tests: add replay tests to confidential MPT (#6451) 2026-03-04 13:43:43 -05:00
yinyiqian1 c52d317810 Update hashing and support ticket (#6444) 2026-02-27 11:50:22 -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 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
+2 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: Jingchen <[email protected]>
Co-authored-by: Denis Angell <[email protected]>
Co-authored-by: Bart <[email protected]>
Co-authored-by: yinyiqian1 <[email protected]>
Co-authored-by: Vito Tumas <[email protected]>
Co-authored-by: Bronek Kozicki <[email protected]>
Co-authored-by: Mayukha Vadari <[email protected]>
Co-authored-by: Valentin Balaschenko <[email protected]>
Co-authored-by: tequ <[email protected]>
Co-authored-by: Ayaz Salikhov <[email protected]>
2025-10-07 14:14:34 -04:00
Shawn Xie c3fdbc0430 SFields and formats (#5795) 2025-10-01 17:02:11 +00:00
68 changed files with 21177 additions and 253 deletions

No files matched your search

+2
View File
@@ -90,6 +90,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)
@@ -102,6 +103,7 @@ target_link_libraries(
INTERFACE
ed25519::ed25519
lz4::lz4
mpt-crypto::mpt-crypto
OpenSSL::Crypto
OpenSSL::SSL
secp256k1::secp256k1
+1
View File
@@ -12,6 +12,7 @@
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
"mpt-crypto/0.4.0-rc2#a580f2f9ad0e795de696aa62d54fb9af%1782425834.488828",
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
"libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732",
+2
View File
@@ -30,6 +30,7 @@ class Xrpl(ConanFile):
"ed25519/2015.03",
"grpc/1.81.1",
"libarchive/3.8.7",
"mpt-crypto/0.4.0-rc2",
"nudb/2.0.9",
"openssl/3.6.3",
"secp256k1/0.7.1",
@@ -208,6 +209,7 @@ class Xrpl(ConanFile):
"grpc::grpc++",
"libarchive::libarchive",
"lz4::lz4",
"mpt-crypto::mpt-crypto",
"nudb::nudb",
"openssl::crypto",
"protobuf::libprotobuf",
+11
View File
@@ -60,6 +60,7 @@ words:
- autobridging
- bimap
- bindir
- blindings
- bookdir
- Bougalis
- Britto
@@ -95,6 +96,7 @@ words:
- daria
- dcmake
- dearmor
- decryptor
- dedented
- deleteme
- demultiplexer
@@ -106,6 +108,7 @@ words:
- distro
- doxyfile
- dxrpl
- elgamal
- enabled
- enablerepo
- endmacro
@@ -119,6 +122,7 @@ words:
- fmtdur
- fsanitize
- funclets
- Gamal
- gcov
- gcovr
- ghead
@@ -216,6 +220,7 @@ words:
- partitioner
- paychan
- paychans
- Pedersen
- permdex
- perminute
- permissioned
@@ -239,6 +244,10 @@ words:
- Raphson
- rcflags
- replayer
- rerandomize
- rerandomization
- rerandomized
- rerandomizes
- rerere
- retriable
- RIPD
@@ -255,6 +264,7 @@ words:
- sahyadri
- Satoshi
- scons
- Schnorr
- secp
- sendq
- seqit
@@ -285,6 +295,7 @@ words:
- stvar
- stvector
- stxchainattestations
- summands
- superpeer
- superpeers
- takergets
@@ -63,6 +63,39 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j);
TER
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j);
/**
* @brief Check whether src is authorized to deposit to dst.
*
* @param tx Transaction containing optional credential IDs.
* @param view Read-only ledger view.
* @param src Source account.
* @param dst Destination account.
* @param sleDst Destination AccountRoot, if it exists.
* @param j Journal for diagnostics.
* @return tesSUCCESS if the deposit is allowed, otherwise an authorization
* error.
*/
TER
checkDepositPreauth(
STTx const& tx,
ReadView const& view,
AccountID const& src,
AccountID const& dst,
std::shared_ptr<SLE const> const& sleDst,
beast::Journal j);
/**
* @brief Remove expired credentials referenced by the transaction.
*
* @param tx Transaction containing optional sfCredentialIDs.
* @param view Mutable ledger view.
* @param j Journal for diagnostics.
* @return tesSUCCESS if no referenced credentials expired, tecEXPIRED if any
* were removed, or an error from credential deletion.
*/
TER
cleanupExpiredCredentials(STTx const& tx, ApplyView& view, beast::Journal j);
// Check expired credentials and for existing DepositPreauth ledger object
TER
verifyDepositPreauth(
@@ -0,0 +1,431 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.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>
#include <cstdint>
#include <limits>
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
{
/** @brief The recipient's ElGamal public key (size=xrpl::kEcPubKeyLength). */
Slice publicKey;
/**
* @brief The encrypted amount ciphertext
* (size=xrpl::kEcGamalEncryptedTotalLength).
*/
Slice encryptedAmount;
};
/**
* @brief Holds two secp256k1 public key components representing an ElGamal
* ciphertext (C1, C2).
*/
struct EcPair
{
/** @brief First ElGamal ciphertext component. */
secp256k1_pubkey c1;
/** @brief Second ElGamal ciphertext component. */
secp256k1_pubkey c2;
};
/**
* @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, wrapping back to 0 at UINT32_MAX.
// The wrap is computed explicitly rather than relying on unsigned overflow
// of `+ 1u`, as it trips the unsigned-integer-overflow sanitizer in the UBSan CI build.
auto const current = mptoken[~sfConfidentialBalanceVersion].valueOr(0u);
mptoken[sfConfidentialBalanceVersion] =
current == std::numeric_limits<std::uint32_t>::max() ? 0u : current + 1u;
}
/**
* @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 issuanceID The MPToken Issuance ID.
* @param sequence The transaction sequence number or ticket number.
* @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,
uint192 const& issuanceID,
std::uint32_t sequence,
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 issuanceID The MPToken Issuance ID.
* @param sequence The transaction sequence number or ticket number.
* @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,
uint192 const& issuanceID,
std::uint32_t sequence,
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 issuanceID The MPToken Issuance ID.
* @param sequence The transaction sequence number or a ticket number.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence);
/**
* @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 issuanceID The MPToken Issuance ID.
* @param sequence The transaction sequence number or a ticket number.
* @param version The holder's confidential balance version.
* @return A 256-bit context hash unique to this transaction.
*/
uint256
getConvertBackContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
std::uint32_t version);
/**
* @brief Parses an ElGamal ciphertext into two secp256k1 public key components.
*
* Breaks an encrypted amount (size=xrpl::kEcGamalEncryptedTotalLength, two
* compressed EC points of size=xrpl::kEcCiphertextComponentLength) into
* a pair containing (C1, C2) for use in cryptographic operations.
*
* @param buffer The buffer containing the compressed ciphertext
* (size=xrpl::kEcGamalEncryptedTotalLength).
* @return The parsed pair (c1, c2) if successful, std::nullopt if the buffer is invalid.
*/
std::optional<EcPair>
makeEcPair(Slice const& buffer);
/**
* @brief Serializes an EcPair into compressed form.
*
* Converts an EcPair (C1, C2) back into a buffer
* (size=xrpl::kEcGamalEncryptedTotalLength) containing two compressed EC
* points (size=xrpl::kEcCiphertextComponentLength each).
*
* @param pair The EcPair to serialize.
* @return The buffer (size=xrpl::kEcGamalEncryptedTotalLength), or std::nullopt
* if serialization fails.
*/
std::optional<Buffer>
serializeEcPair(EcPair const& pair);
/**
* @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
* (size=xrpl::kCompressedEcPointLength).
* @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 (size=xrpl::kEcGamalEncryptedTotalLength).
* @param b The second ciphertext (size=xrpl::kEcGamalEncryptedTotalLength).
* @return The resulting ciphertext Enc(a + b), or std::nullopt on failure.
*/
std::optional<Buffer>
homomorphicAdd(Slice const& a, Slice const& b);
/**
* @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 (size=xrpl::kEcGamalEncryptedTotalLength).
* @param b The subtrahend ciphertext (size=xrpl::kEcGamalEncryptedTotalLength).
* @return The resulting ciphertext Enc(a - b), or std::nullopt on failure.
*/
std::optional<Buffer>
homomorphicSubtract(Slice const& a, Slice const& b);
/**
* @brief Re-randomizes an ElGamal ciphertext without changing its plaintext.
*
* Adds Enc(0; randomness) under the supplied public key to the ciphertext.
* This is used when a public, deterministic scalar must perturb ciphertext
* randomness while preserving ledger reproducibility.
*
* @param ciphertext The ciphertext to re-randomize
* (size=xrpl::kEcGamalEncryptedTotalLength).
* @param pubKeySlice The ElGamal public key matching the ciphertext recipient.
* @param randomness The scalar used as zero-encryption randomness
* (size=xrpl::kEcScalarLength).
* @return The re-randomized ciphertext, or std::nullopt on failure.
*/
std::optional<Buffer>
rerandomizeCiphertext(Slice const& ciphertext, Slice const& pubKeySlice, Slice const& randomness);
/**
* @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 (size=xrpl::kEcPubKeyLength).
* @param blindingFactor The randomness used as blinding factor r
* (size=xrpl::ecBlindingFactorLength).
* @return The ciphertext (size=xrpl::kEcGamalEncryptedTotalLength), 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 (size=xrpl::kEcPubKeyLength).
* @param account The account ID of the token holder.
* @param mptId The MPToken Issuance ID.
* @return The canonical zero ciphertext (size=xrpl::kEcGamalEncryptedTotalLength), 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 (size=xrpl::kEcPubKeyLength).
* @param proofSlice The Schnorr proof (size=xrpl::ecSchnorrProofLength).
* @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 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 blinding factor used in all encryptions
* (size=xrpl::ecBlindingFactorLength).
* @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(
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 uint8_t
getConfidentialRecipientCount(bool hasAuditor)
{
return hasAuditor ? 4 : 3;
}
/**
* @brief Verifies a compact sigma clawback 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 (ecClawbackProofLength).
* @param pubKeySlice The issuer's ElGamal public key (kEcPubKeyLength bytes).
* @param ciphertext The issuer's encrypted balance on the holder's account
* (kEcGamalEncryptedTotalLength bytes).
* @param contextHash The 256-bit context hash binding the proof.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyClawbackProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
Slice const& ciphertext,
uint256 const& contextHash);
/**
* @brief Generates a cryptographically secure blinding factor
* (size=xrpl::kEcBlindingFactorLength).
*
* Produces random bytes suitable for use as an ElGamal blinding factor
* or Pedersen commitment randomness.
*
* @return A buffer containing the random blinding factor
* (size=xrpl::kEcBlindingFactorLength).
*/
Buffer
generateBlindingFactor();
/**
* @brief Verifies all zero-knowledge proofs for a ConfidentialMPTSend transaction.
*
* This function calls mpt_verify_send_proof API in the mpt-crypto utility lib, which verifies the
* equality proof, amount linkage, balance linkage, and range proof.
* Equality proof: Proves the same value is encrypted for the sender, receiver, issuer, and auditor.
* Amount linkage: Proves the send amount matches the amount Pedersen commitment.
* Balance linkage: Proves the sender's balance matches the balance Pedersen
* commitment.
* Range proof: Proves the amount and the remaining balance are within range [0, 2^64-1].
*
* @param proof The full proof blob.
* @param sender The sender's public key and encrypted amount.
* @param destination The destination's public key and encrypted amount.
* @param issuer The issuer's public key and encrypted amount.
* @param auditor The auditor's public key and encrypted amount if present.
* @param spendingBalance The sender's current spending balance ciphertext.
* @param amountCommitment The Pedersen commitment to the send amount.
* @param balanceCommitment The Pedersen commitment to the sender's balance.
* @param contextHash The context hash binding the proof.
* @return tesSUCCESS if all proofs are valid, or an error code otherwise.
*/
TER
verifySendProof(
Slice const& proof,
ConfidentialRecipient const& sender,
ConfidentialRecipient const& destination,
ConfidentialRecipient const& issuer,
std::optional<ConfidentialRecipient> const& auditor,
Slice const& spendingBalance,
Slice const& amountCommitment,
Slice const& balanceCommitment,
uint256 const& contextHash);
/**
* @brief Verifies all zero-knowledge proofs for a ConfidentialMPTConvertBack transaction.
*
* This function calls mpt_verify_convert_back_proof API in the mpt-crypto utility lib, which
* verifies the balance linkage proof and range proof. Balance linkage proof: proves the balance
* commitment matches the spending ciphertext. Range proof: proves the remaining balance after
* convert back is within range [0, 2^64-1].
*
* @param proof The full proof blob.
* @param pubKeySlice The holder's public key.
* @param spendingBalance The holder's spending balance ciphertext.
* @param balanceCommitment The Pedersen commitment to the balance.
* @param amount The amount being converted back to public.
* @param contextHash The context hash binding the proof.
* @return tesSUCCESS if all proofs are valid, or an error code otherwise.
*/
TER
verifyConvertBackProof(
Slice const& proof,
Slice const& pubKeySlice,
Slice const& spendingBalance,
Slice const& balanceCommitment,
uint64_t amount,
uint256 const& contextHash);
} // namespace xrpl
+4 -2
View File
@@ -177,7 +177,8 @@ enum LedgerEntryType : std::uint16_t {
LSF_FLAG(lsfMPTCanEscrow, 0x00000008) \
LSF_FLAG(lsfMPTCanTrade, 0x00000010) \
LSF_FLAG(lsfMPTCanTransfer, 0x00000020) \
LSF_FLAG(lsfMPTCanClawback, 0x00000040)) \
LSF_FLAG(lsfMPTCanClawback, 0x00000040) \
LSF_FLAG(lsfMPTCanHoldConfidentialBalance, 0x00000080)) \
\
LEDGER_OBJECT(MPTokenIssuanceMutable, \
LSF_FLAG(lsmfMPTCanEnableCanLock, 0x00000002) \
@@ -186,8 +187,9 @@ enum LedgerEntryType : std::uint16_t {
LSF_FLAG(lsmfMPTCanEnableCanTrade, 0x00000010) \
LSF_FLAG(lsmfMPTCanEnableCanTransfer, 0x00000020) \
LSF_FLAG(lsmfMPTCanEnableCanClawback, 0x00000040) \
LSF_FLAG(lsmfMPTCannotEnableCanHoldConfidentialBalance, 0x00000080) \
LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000) \
LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \
LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \
\
LEDGER_OBJECT(MPToken, \
LSF_FLAG2(lsfMPTLocked, 0x00000001) \
+62
View File
@@ -4,6 +4,10 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Units.h>
#include <mpt_protocol.h>
#include <secp256k1_mpt.h>
#include <cstddef>
#include <cstdint>
namespace xrpl {
@@ -307,4 +311,62 @@ constexpr std::size_t kPermissionMaxSize = 10;
/** The maximum number of transactions that can be in a batch. */
constexpr std::size_t kMaxBatchTxCount = 8;
/** Length of a secp256k1 scalar in bytes. */
constexpr std::size_t kEcScalarLength = kMPT_SCALAR_SIZE;
/** Length of EC point (compressed) */
constexpr std::size_t kCompressedEcPointLength = 33;
/** Length of one compressed EC point component in an EC ElGamal ciphertext. */
constexpr std::size_t kEcCiphertextComponentLength = kMPT_ELGAMAL_CIPHER_SIZE;
/** EC ElGamal ciphertext length: two compressed EC points concatenated. */
constexpr std::size_t kEcGamalEncryptedTotalLength = kMPT_ELGAMAL_TOTAL_SIZE;
/** Length of EC public key (compressed) */
constexpr std::size_t kEcPubKeyLength = kMPT_PUBKEY_SIZE;
/** Length of EC private key in bytes */
constexpr std::size_t kEcPrivKeyLength = kMPT_PRIVKEY_SIZE;
/** Length of the EC blinding factor in bytes */
constexpr std::size_t kEcBlindingFactorLength = kMPT_BLINDING_FACTOR_SIZE;
/** Length of Schnorr ZKProof for public key registration (compact form) in bytes */
constexpr std::size_t kEcSchnorrProofLength = kMPT_SCHNORR_PROOF_SIZE;
/** Length of Pedersen Commitment (compressed) */
constexpr std::size_t kEcPedersenCommitmentLength = kMPT_PEDERSEN_COMMIT_SIZE;
/** Length of single bulletproof (range proof for 1 commitment) in bytes */
constexpr std::size_t kEcSingleBulletproofLength = kMPT_SINGLE_BULLETPROOF_SIZE;
/** Length of double bulletproof (range proof for 2 commitments) in bytes */
constexpr std::size_t kEcDoubleBulletproofLength = kMPT_DOUBLE_BULLETPROOF_SIZE;
/** Length of the compact sigma proof component for ConfidentialMPTSend. */
constexpr std::size_t kEcSendSigmaProofLength = SECP256K1_COMPACT_STANDARD_PROOF_SIZE;
/** 192 bytes compact sigma proof + 754 bytes double bulletproof. */
constexpr std::size_t kEcSendProofLength = kEcSendSigmaProofLength + kEcDoubleBulletproofLength;
/** Length of the compact sigma proof component for ConfidentialMPTConvertBack. */
constexpr std::size_t kEcConvertBackSigmaProofLength = SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE;
/** 128 bytes compact sigma proof + 688 bytes single bulletproof. */
constexpr std::size_t kEcConvertBackProofLength =
kEcConvertBackSigmaProofLength + kEcSingleBulletproofLength;
/** Length of the ZKProof for ConfidentialMPTClawback. */
constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE;
/** Extra base fee multiplier charged to confidential MPT transactions. */
constexpr std::uint32_t kConfidentialFeeMultiplier = 9;
/** Compressed EC point prefix for even y-coordinate */
constexpr std::uint8_t kEcCompressedPrefixEvenY = 0x02;
/** Compressed EC point prefix for odd y-coordinate */
constexpr std::uint8_t kEcCompressedPrefixOddY = 0x03;
} // namespace xrpl
+6
View File
@@ -128,6 +128,7 @@ enum TEMcodes : TERUnderlyingType {
temBAD_TRANSFER_FEE,
temINVALID_INNER_BATCH,
temBAD_MPT,
temBAD_CIPHERTEXT,
};
//------------------------------------------------------------------------------
@@ -358,6 +359,11 @@ enum TECcodes : TERUnderlyingType {
tecLIMIT_EXCEEDED = 195,
tecPSEUDO_ACCOUNT = 196,
tecPRECISION_LOSS = 197,
// DEPRECATED: This error code tecNO_DELEGATE_PERMISSION is reserved for
// backward compatibility with historical data on non-prod networks, can be
// reclaimed after those networks reset.
tecNO_DELEGATE_PERMISSION = 198,
tecBAD_PROOF = 199,
};
//------------------------------------------------------------------------------
+8 -3
View File
@@ -140,7 +140,8 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
TF_FLAG(tfMPTCanEscrow, lsfMPTCanEscrow) \
TF_FLAG(tfMPTCanTrade, lsfMPTCanTrade) \
TF_FLAG(tfMPTCanTransfer, lsfMPTCanTransfer) \
TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback), \
TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback) \
TF_FLAG(tfMPTCanHoldConfidentialBalance, lsfMPTCanHoldConfidentialBalance), \
MASK_ADJ(0)) \
\
TRANSACTION(MPTokenAuthorize, \
@@ -349,10 +350,13 @@ inline constexpr FlagValue tmfMPTCanEnableCanTransfer = lsmfMPTCanEnableCanTrans
inline constexpr FlagValue tmfMPTCanEnableCanClawback = lsmfMPTCanEnableCanClawback;
inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata;
inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee;
inline constexpr FlagValue tmfMPTCannotEnableCanHoldConfidentialBalance =
lsmfMPTCannotEnableCanHoldConfidentialBalance;
inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask =
~(tmfMPTCanEnableCanLock | tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanEscrow |
tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanEnableCanClawback |
tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee);
tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee |
tmfMPTCannotEnableCanHoldConfidentialBalance);
// MPTokenIssuanceSet MutableFlags:
// Enable mutable capability flags. These flags are one-way: once enabled,
@@ -364,9 +368,10 @@ inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000004;
inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000008;
inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000010;
inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000020;
inline constexpr FlagValue tmfMPTSetCanHoldConfidentialBalance = 0x00000040;
inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask =
~(tmfMPTSetCanLock | tmfMPTSetRequireAuth | tmfMPTSetCanEscrow | tmfMPTSetCanTrade |
tmfMPTSetCanTransfer | tmfMPTSetCanClawback);
tmfMPTSetCanTransfer | tmfMPTSetCanClawback | tmfMPTSetCanHoldConfidentialBalance);
// Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between accounts allowed a
// TrustLine to be added to the issuer of that token without explicit permission from that issuer.
+1 -1
View File
@@ -14,7 +14,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo)
@@ -401,19 +401,28 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
{sfDomainID, SoeOptional},
{sfMutableFlags, SoeDefault},
{sfReferenceHolding, SoeOptional},
{sfIssuerEncryptionKey, SoeOptional},
{sfAuditorEncryptionKey, SoeOptional},
{sfConfidentialOutstandingAmount, SoeDefault},
}))
/** A ledger object which tracks MPToken
\sa keylet::mptoken
*/
LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfAccount, SoeRequired},
{sfMPTokenIssuanceID, SoeRequired},
{sfMPTAmount, SoeDefault},
{sfLockedAmount, SoeOptional},
{sfOwnerNode, SoeRequired},
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
{sfAccount, SoeRequired},
{sfMPTokenIssuanceID, SoeRequired},
{sfMPTAmount, SoeDefault},
{sfLockedAmount, SoeOptional},
{sfOwnerNode, SoeRequired},
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
{sfConfidentialBalanceInbox, SoeOptional},
{sfConfidentialBalanceSpending, SoeOptional},
{sfConfidentialBalanceVersion, SoeDefault},
{sfIssuerEncryptedBalance, SoeOptional},
{sfAuditorEncryptedBalance, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
}))
/** A ledger object which tracks Oracle
+4 -1
View File
@@ -11,7 +11,10 @@ secp256k1Context()
struct Holder
{
secp256k1_context* impl;
Holder() : impl(secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN))
// SECP256K1_CONTEXT_SIGN and SECP256K1_CONTEXT_VERIFY were deprecated.
// All contexts support both signing and verification, so
// SECP256K1_CONTEXT_NONE is the correct flag to use.
Holder() : impl(secp256k1_context_create(SECP256K1_CONTEXT_NONE))
{
}
@@ -113,6 +113,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)
@@ -146,6 +147,7 @@ TYPED_SFIELD(sfSubjectNode, UINT64, 28)
TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::kSmdBaseTen|SField::kSmdDefault)
TYPED_SFIELD(sfVaultNode, UINT64, 30)
TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31)
TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::kSmdBaseTen|SField::kSmdDefault)
// 128-bit
TYPED_SFIELD(sfEmailHash, UINT128, 1)
@@ -206,6 +208,7 @@ TYPED_SFIELD(sfLoanBrokerID, UINT256, 37,
SField::kSmdPseudoAccount | SField::kSmdDefault)
TYPED_SFIELD(sfLoanID, UINT256, 38)
TYPED_SFIELD(sfReferenceHolding, UINT256, 39)
TYPED_SFIELD(sfBlindingFactor, UINT256, 40)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
@@ -299,6 +302,21 @@ 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(sfIssuerEncryptionKey, VL, 35)
TYPED_SFIELD(sfHolderEncryptionKey, 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(sfAuditorEncryptionKey, VL, 44)
TYPED_SFIELD(sfAmountCommitment, VL, 45)
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
@@ -735,6 +735,8 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
{sfMPTokenMetadata, SoeOptional},
{sfTransferFee, SoeOptional},
{sfMutableFlags, SoeOptional},
{sfIssuerEncryptionKey, SoeOptional},
{sfAuditorEncryptionKey, SoeOptional},
}))
/** This transaction type authorizes a MPToken instance */
@@ -1077,6 +1079,91 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay,
{sfAmount, SoeRequired, SoeMptSupported},
}))
/** This transaction type converts into confidential MPT balance. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/token/ConfidentialMPTConvert.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert,
Delegation::Delegable,
featureConfidentialTransfer,
NoPriv,
({
{sfMPTokenIssuanceID, SoeRequired},
{sfMPTAmount, SoeRequired},
{sfHolderEncryptionKey, SoeOptional},
{sfHolderEncryptedAmount, SoeRequired},
{sfIssuerEncryptedAmount, SoeRequired},
{sfAuditorEncryptedAmount, SoeOptional},
{sfBlindingFactor, SoeRequired},
{sfZKProof, SoeOptional},
}))
/** This transaction type merges MPT inbox. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/token/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 <xrpl/tx/transactors/token/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 <xrpl/tx/transactors/token/ConfidentialMPTSend.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend,
Delegation::Delegable,
featureConfidentialTransfer,
NoPriv,
({
{sfMPTokenIssuanceID, SoeRequired},
{sfDestination, SoeRequired},
{sfDestinationTag, SoeOptional},
{sfSenderEncryptedAmount, SoeRequired},
{sfDestinationEncryptedAmount, SoeRequired},
{sfIssuerEncryptedAmount, SoeRequired},
{sfAuditorEncryptedAmount, SoeOptional},
{sfZKProof, SoeRequired},
{sfAmountCommitment, SoeRequired},
{sfBalanceCommitment, SoeRequired},
{sfCredentialIDs, SoeOptional},
}))
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/token/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
+168 -162
View File
@@ -137,6 +137,7 @@ JSS(authorized_credentials); // in: ledger_entry DepositPreauth
JSS(auth_accounts); // out: amm_info
JSS(auth_change); // out: AccountInfo
JSS(auth_change_queued); // out: AccountInfo
JSS(auditor_encrypted_balance); // out: mpt_holders (confidential MPT)
JSS(available); // out: ValidatorList
JSS(avg_bps_recv); // out: Peers
JSS(avg_bps_sent); // out: Peers
@@ -161,9 +162,6 @@ JSS(build_path); // in: TransactionSign
JSS(build_version); // out: NetworkOPs
JSS(cancel_after); // out: AccountChannels
JSS(can_delete); // out: CanDelete
JSS(mpt_amount); // out: mpt_holders
JSS(mpt_issuance_id); // in: Payment, mpt_holders
JSS(mptoken_index); // out: mpt_holders
JSS(changes); // out: BookChanges
JSS(channel_id); // out: AccountChannels
JSS(channels); // out: AccountChannels
@@ -185,165 +183,170 @@ JSS(command); // in: RPCHandler
JSS(common); // out: RPC server_definitions
JSS(complete); // out: NetworkOPs, InboundLedger
JSS(complete_ledgers); // out: NetworkOPs, PeerImp
JSS(consensus); // out: NetworkOPs, LedgerConsensus
JSS(converge_time); // out: NetworkOPs
JSS(converge_time_s); // out: NetworkOPs
JSS(cookie); // out: NetworkOPs
JSS(count); // in: AccountTx*, ValidatorList
JSS(counters); // in/out: retrieve counters
JSS(credentials); // in: deposit_authorized
JSS(credential_type); // in: LedgerEntry DepositPreauth
JSS(ctid); // in/out: Tx RPC
JSS(currency_a); // out: BookChanges
JSS(currency_b); // out: BookChanges
JSS(currency); // in: paths/PathRequest, STAmount
// out: STPathSet, STAmount, AccountLines
JSS(current); // out: OwnerInfo
JSS(current_activities); //
JSS(current_ledger_size); // out: TxQ
JSS(current_queue_size); // out: TxQ
JSS(data); // out: LedgerData
JSS(date); // out: tx/Transaction, NetworkOPs
JSS(dbKBLedger); // out: getCounts
JSS(dbKBTotal); // out: getCounts
JSS(dbKBTransaction); // out: getCounts
JSS(debug_signing); // in: TransactionSign
JSS(deletion_blockers_only); // in: AccountObjects
JSS(delivered_amount); // out: insertDeliveredAmount
JSS(deposit_authorized); // out: deposit_authorized
JSS(deprecated); //
JSS(descending); // in: AccountTx*
JSS(description); // in/out: Reservations
JSS(destination); // in: nft_buy_offers, nft_sell_offers
JSS(destination_account); // in: PathRequest, RipplePathFind, account_lines
// out: AccountChannels
JSS(destination_amount); // in: PathRequest, RipplePathFind
JSS(destination_currencies); // in: PathRequest, RipplePathFind
JSS(destination_tag); // in: PathRequest
// out: AccountChannels
JSS(details); // out: Manifest, server_info
JSS(dir_entry); // out: DirectoryEntryIterator
JSS(dir_index); // out: DirectoryEntryIterator
JSS(dir_root); // out: DirectoryEntryIterator
JSS(discounted_fee); // out: amm_info
JSS(domain); // out: ValidatorInfo, Manifest
JSS(drops); // out: TxQ
JSS(duration_us); // out: NetworkOPs
JSS(effective); // out: ValidatorList
// in: UNL
JSS(enabled); // out: AmendmentTable
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit
JSS(entire_set); // out: get_aggregate_price
JSS(ephemeral_key); // out: ValidatorInfo
// in/out: Manifest
JSS(error); // out: error
JSS(errored); //
JSS(error_code); // out: error
JSS(error_exception); // out: Submit
JSS(error_message); // out: error
JSS(expand); // in: handler/Ledger
JSS(expected_date); // out: any (warnings)
JSS(expected_date_UTC); // out: any (warnings)
JSS(expected_ledger_size); // out: TxQ
JSS(expiration); // out: AccountOffers, AccountChannels, ValidatorList, amm_info
JSS(fail_hard); // in: Sign, Submit
JSS(failed); // out: InboundLedger
JSS(feature); // in: Feature
JSS(features); // out: Feature
JSS(fee_base); // out: NetworkOPs
JSS(fee_div_max); // in: TransactionSign
JSS(fee_level); // out: AccountInfo
JSS(fee_mult_max); // in: TransactionSign
JSS(fee_ref); // out: NetworkOPs, DEPRECATED
JSS(fetch_pack); // out: NetworkOPs
JSS(FIELDS); // out: RPC server_definitions
// matches definitions.json format
JSS(first); // out: rpc/Version
JSS(finished); //
JSS(fix_txns); // in: LedgerCleaner
JSS(flags); // out: AccountOffers, NetworkOPs
JSS(forward); // in: AccountTx
JSS(freeze); // out: AccountLines
JSS(freeze_peer); // out: AccountLines
JSS(deep_freeze); // out: AccountLines
JSS(deep_freeze_peer); // out: AccountLines
JSS(frozen_balances); // out: GatewayBalances
JSS(full); // in: LedgerClearer, handlers/Ledger
JSS(full_reply); // out: PathFind
JSS(fullbelow_size); // out: GetCounts
JSS(git); // out: server_info
JSS(good); // out: RPCVersion
JSS(hash); // out: NetworkOPs, InboundLedger, LedgerToJson, STTx; field
JSS(have_header); // out: InboundLedger
JSS(have_state); // out: InboundLedger
JSS(have_transactions); // out: InboundLedger
JSS(high); // out: BookChanges
JSS(highest_sequence); // out: AccountInfo
JSS(highest_ticket); // out: AccountInfo
JSS(historical_perminute); // historical_perminute.
JSS(holders); // out: MPTHolders
JSS(hostid); // out: NetworkOPs
JSS(hotwallet); // in: GatewayBalances
JSS(id); // websocket.
JSS(ident); // in: AccountCurrencies, AccountInfo, OwnerInfo
JSS(ignore_default); // in: AccountLines
JSS(in); // out: OverlayImpl
JSS(inLedger); // out: tx/Transaction
JSS(inbound); // out: PeerImp
JSS(index); // in: LedgerEntry
// out: STLedgerEntry, LedgerEntry, TxHistory, LedgerData
JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo
JSS(initial_sync_duration_us); //
JSS(internal_command); // in: Internal
JSS(invalid_API_version); // out: Many, when a request has an invalid version
JSS(io_latency_ms); // out: NetworkOPs
JSS(ip); // in: Connect, out: OverlayImpl
JSS(is_burned); // out: nft_info (clio)
JSS(isSerialized); // out: RPC server_definitions
// matches definitions.json format
JSS(isSigningField); // out: RPC server_definitions
// matches definitions.json format
JSS(isVLEncoded); // out: RPC server_definitions
// matches definitions.json format
JSS(issuer); // in: RipplePathFind, Subscribe, Unsubscribe, BookOffers
// out: STPathSet, STAmount
JSS(job); //
JSS(job_queue); //
JSS(jobs); //
JSS(jsonrpc); // json version
JSS(jq_trans_overflow); // JobQueue transaction limit overflow.
JSS(kept); // out: SubmitTransaction
JSS(key); // out
JSS(key_type); // in/out: WalletPropose, TransactionSign
JSS(latency); // out: PeerImp
JSS(last); // out: RPCVersion
JSS(last_close); // out: NetworkOPs
JSS(last_refresh_time); // out: ValidatorSite
JSS(last_refresh_status); // out: ValidatorSite
JSS(last_refresh_message); // out: ValidatorSite
JSS(ledger); // in: NetworkOPs, LedgerCleaner, RPCHelpers
// out: NetworkOPs, PeerImp
JSS(ledger_current_index); // out: NetworkOPs, RPCHelpers, LedgerCurrent, LedgerAccept,
// AccountLines
JSS(ledger_data); // out: LedgerHeader
JSS(ledger_hash); // in: RPCHelpers, LedgerRequest, RipplePathFind,
// TransactionEntry, handlers/Ledger
// out: NetworkOPs, RPCHelpers, LedgerClosed, LedgerData,
// AccountLines
JSS(ledger_hit_rate); // out: GetCounts
JSS(ledger_index); // in/out: many
JSS(ledger_index_max); // in, out: AccountTx*
JSS(ledger_index_min); // in, out: AccountTx*
JSS(ledger_max); // in, out: AccountTx*
JSS(ledger_min); // in, out: AccountTx*
JSS(ledger_time); // out: NetworkOPs
JSS(LEDGER_ENTRY_TYPES); // out: RPC server_definitions
// matches definitions.json format
JSS(LEDGER_ENTRY_FLAGS); // out: RPC server_definitions
JSS(LEDGER_ENTRY_FORMATS); // out: RPC server_definitions
JSS(levels); // LogLevels
JSS(confidential_balance_inbox); // out: mpt_holders (confidential MPT)
JSS(confidential_balance_spending); // out: mpt_holders (confidential MPT)
JSS(confidential_balance_version); // out: mpt_holders (confidential MPT)
JSS(consensus); // out: NetworkOPs, LedgerConsensus
JSS(converge_time); // out: NetworkOPs
JSS(converge_time_s); // out: NetworkOPs
JSS(cookie); // out: NetworkOPs
JSS(count); // in: AccountTx*, ValidatorList
JSS(counters); // in/out: retrieve counters
JSS(credentials); // in: deposit_authorized
JSS(credential_type); // in: LedgerEntry DepositPreauth
JSS(ctid); // in/out: Tx RPC
JSS(currency_a); // out: BookChanges
JSS(currency_b); // out: BookChanges
JSS(currency); // in: paths/PathRequest, STAmount
// out: STPathSet, STAmount, AccountLines
JSS(current); // out: OwnerInfo
JSS(current_activities); //
JSS(current_ledger_size); // out: TxQ
JSS(current_queue_size); // out: TxQ
JSS(data); // out: LedgerData
JSS(date); // out: tx/Transaction, NetworkOPs
JSS(dbKBLedger); // out: getCounts
JSS(dbKBTotal); // out: getCounts
JSS(dbKBTransaction); // out: getCounts
JSS(debug_signing); // in: TransactionSign
JSS(deletion_blockers_only); // in: AccountObjects
JSS(delivered_amount); // out: insertDeliveredAmount
JSS(deposit_authorized); // out: deposit_authorized
JSS(deprecated); //
JSS(descending); // in: AccountTx*
JSS(description); // in/out: Reservations
JSS(destination); // in: nft_buy_offers, nft_sell_offers
JSS(destination_account); // in: PathRequest, RipplePathFind, account_lines
// out: AccountChannels
JSS(destination_amount); // in: PathRequest, RipplePathFind
JSS(destination_currencies); // in: PathRequest, RipplePathFind
JSS(destination_tag); // in: PathRequest
// out: AccountChannels
JSS(details); // out: Manifest, server_info
JSS(dir_entry); // out: DirectoryEntryIterator
JSS(dir_index); // out: DirectoryEntryIterator
JSS(dir_root); // out: DirectoryEntryIterator
JSS(discounted_fee); // out: amm_info
JSS(domain); // out: ValidatorInfo, Manifest
JSS(drops); // out: TxQ
JSS(duration_us); // out: NetworkOPs
JSS(effective); // out: ValidatorList
// in: UNL
JSS(enabled); // out: AmendmentTable
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit
JSS(entire_set); // out: get_aggregate_price
JSS(ephemeral_key); // out: ValidatorInfo
// in/out: Manifest
JSS(error); // out: error
JSS(errored); //
JSS(error_code); // out: error
JSS(error_exception); // out: Submit
JSS(error_message); // out: error
JSS(expand); // in: handler/Ledger
JSS(expected_date); // out: any (warnings)
JSS(expected_date_UTC); // out: any (warnings)
JSS(expected_ledger_size); // out: TxQ
JSS(expiration); // out: AccountOffers, AccountChannels, ValidatorList, amm_info
JSS(fail_hard); // in: Sign, Submit
JSS(failed); // out: InboundLedger
JSS(feature); // in: Feature
JSS(features); // out: Feature
JSS(fee_base); // out: NetworkOPs
JSS(fee_div_max); // in: TransactionSign
JSS(fee_level); // out: AccountInfo
JSS(fee_mult_max); // in: TransactionSign
JSS(fee_ref); // out: NetworkOPs, DEPRECATED
JSS(fetch_pack); // out: NetworkOPs
JSS(FIELDS); // out: RPC server_definitions
// matches definitions.json format
JSS(first); // out: rpc/Version
JSS(finished); //
JSS(fix_txns); // in: LedgerCleaner
JSS(flags); // out: AccountOffers, NetworkOPs
JSS(forward); // in: AccountTx
JSS(freeze); // out: AccountLines
JSS(freeze_peer); // out: AccountLines
JSS(deep_freeze); // out: AccountLines
JSS(deep_freeze_peer); // out: AccountLines
JSS(frozen_balances); // out: GatewayBalances
JSS(full); // in: LedgerClearer, handlers/Ledger
JSS(full_reply); // out: PathFind
JSS(fullbelow_size); // out: GetCounts
JSS(git); // out: server_info
JSS(good); // out: RPCVersion
JSS(hash); // out: NetworkOPs, InboundLedger, LedgerToJson, STTx; field
JSS(have_header); // out: InboundLedger
JSS(have_state); // out: InboundLedger
JSS(have_transactions); // out: InboundLedger
JSS(high); // out: BookChanges
JSS(highest_sequence); // out: AccountInfo
JSS(highest_ticket); // out: AccountInfo
JSS(historical_perminute); // historical_perminute.
JSS(holders); // out: MPTHolders
JSS(holder_encryption_key); // out: mpt_holders (confidential MPT)
JSS(hostid); // out: NetworkOPs
JSS(hotwallet); // in: GatewayBalances
JSS(id); // websocket.
JSS(ident); // in: AccountCurrencies, AccountInfo, OwnerInfo
JSS(ignore_default); // in: AccountLines
JSS(in); // out: OverlayImpl
JSS(inLedger); // out: tx/Transaction
JSS(inbound); // out: PeerImp
JSS(index); // in: LedgerEntry
// out: STLedgerEntry, LedgerEntry, TxHistory, LedgerData
JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo
JSS(initial_sync_duration_us); //
JSS(internal_command); // in: Internal
JSS(invalid_API_version); // out: Many, when a request has an invalid version
JSS(io_latency_ms); // out: NetworkOPs
JSS(ip); // in: Connect, out: OverlayImpl
JSS(is_burned); // out: nft_info (clio)
JSS(isSerialized); // out: RPC server_definitions
// matches definitions.json format
JSS(isSigningField); // out: RPC server_definitions
// matches definitions.json format
JSS(isVLEncoded); // out: RPC server_definitions
// matches definitions.json format
JSS(issuer); // in: RipplePathFind, Subscribe, Unsubscribe, BookOffers
// out: STPathSet, STAmount
JSS(issuer_encrypted_balance); // out: mpt_holders (confidential MPT)
JSS(job); //
JSS(job_queue); //
JSS(jobs); //
JSS(jsonrpc); // json version
JSS(jq_trans_overflow); // JobQueue transaction limit overflow.
JSS(kept); // out: SubmitTransaction
JSS(key); // out
JSS(key_type); // in/out: WalletPropose, TransactionSign
JSS(latency); // out: PeerImp
JSS(last); // out: RPCVersion
JSS(last_close); // out: NetworkOPs
JSS(last_refresh_time); // out: ValidatorSite
JSS(last_refresh_status); // out: ValidatorSite
JSS(last_refresh_message); // out: ValidatorSite
JSS(ledger); // in: NetworkOPs, LedgerCleaner, RPCHelpers
// out: NetworkOPs, PeerImp
JSS(ledger_current_index); // out: NetworkOPs, RPCHelpers, LedgerCurrent, LedgerAccept,
// AccountLines
JSS(ledger_data); // out: LedgerHeader
JSS(ledger_hash); // in: RPCHelpers, LedgerRequest, RipplePathFind,
// TransactionEntry, handlers/Ledger
// out: NetworkOPs, RPCHelpers, LedgerClosed, LedgerData,
// AccountLines
JSS(ledger_hit_rate); // out: GetCounts
JSS(ledger_index); // in/out: many
JSS(ledger_index_max); // in, out: AccountTx*
JSS(ledger_index_min); // in, out: AccountTx*
JSS(ledger_max); // in, out: AccountTx*
JSS(ledger_min); // in, out: AccountTx*
JSS(ledger_time); // out: NetworkOPs
JSS(LEDGER_ENTRY_TYPES); // out: RPC server_definitions
// matches definitions.json format
JSS(LEDGER_ENTRY_FLAGS); // out: RPC server_definitions
JSS(LEDGER_ENTRY_FORMATS); // out: RPC server_definitions
JSS(levels); // LogLevels
JSS(limit); // in/out: AccountTx*, AccountOffers, AccountLines, AccountObjects
// in: LedgerData, BookOffers
JSS(limit_peer); // out: AccountLines
@@ -401,6 +404,9 @@ JSS(min_ledger); // in: LedgerCleaner
JSS(minimum_fee); // out: TxQ
JSS(minimum_level); // out: TxQ
JSS(missingCommand); // error
JSS(mpt_amount); // out: mpt_holders
JSS(mpt_issuance_id); // in: Payment, mpt_holders
JSS(mptoken_index); // out: mpt_holders
JSS(mpt_issuance_id_a); // out: BookChanges
JSS(mpt_issuance_id_b); // out: BookChanges
JSS(name); // out: AmendmentTableImpl, PeerImp
@@ -147,6 +147,150 @@ public:
{
return this->sle_->at(sfPreviousTxnLgrSeq);
}
/**
* @brief Get sfConfidentialBalanceInbox (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceInbox() const
{
if (hasConfidentialBalanceInbox())
return this->sle_->at(sfConfidentialBalanceInbox);
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceInbox is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceInbox() const
{
return this->sle_->isFieldPresent(sfConfidentialBalanceInbox);
}
/**
* @brief Get sfConfidentialBalanceSpending (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceSpending() const
{
if (hasConfidentialBalanceSpending())
return this->sle_->at(sfConfidentialBalanceSpending);
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceSpending is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceSpending() const
{
return this->sle_->isFieldPresent(sfConfidentialBalanceSpending);
}
/**
* @brief Get sfConfidentialBalanceVersion (SoeDefault)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getConfidentialBalanceVersion() const
{
if (hasConfidentialBalanceVersion())
return this->sle_->at(sfConfidentialBalanceVersion);
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceVersion is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceVersion() const
{
return this->sle_->isFieldPresent(sfConfidentialBalanceVersion);
}
/**
* @brief Get sfIssuerEncryptedBalance (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getIssuerEncryptedBalance() const
{
if (hasIssuerEncryptedBalance())
return this->sle_->at(sfIssuerEncryptedBalance);
return std::nullopt;
}
/**
* @brief Check if sfIssuerEncryptedBalance is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasIssuerEncryptedBalance() const
{
return this->sle_->isFieldPresent(sfIssuerEncryptedBalance);
}
/**
* @brief Get sfAuditorEncryptedBalance (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getAuditorEncryptedBalance() const
{
if (hasAuditorEncryptedBalance())
return this->sle_->at(sfAuditorEncryptedBalance);
return std::nullopt;
}
/**
* @brief Check if sfAuditorEncryptedBalance is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorEncryptedBalance() const
{
return this->sle_->isFieldPresent(sfAuditorEncryptedBalance);
}
/**
* @brief Get sfHolderEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getHolderEncryptionKey() const
{
if (hasHolderEncryptionKey())
return this->sle_->at(sfHolderEncryptionKey);
return std::nullopt;
}
/**
* @brief Check if sfHolderEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasHolderEncryptionKey() const
{
return this->sle_->isFieldPresent(sfHolderEncryptionKey);
}
};
/**
@@ -270,6 +414,72 @@ public:
return *this;
}
/**
* @brief Set sfConfidentialBalanceInbox (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setConfidentialBalanceInbox(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceInbox] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceSpending (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setConfidentialBalanceSpending(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceSpending] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceVersion (SoeDefault)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setConfidentialBalanceVersion(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfConfidentialBalanceVersion] = value;
return *this;
}
/**
* @brief Set sfIssuerEncryptedBalance (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setIssuerEncryptedBalance(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfIssuerEncryptedBalance] = value;
return *this;
}
/**
* @brief Set sfAuditorEncryptedBalance (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setAuditorEncryptedBalance(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfAuditorEncryptedBalance] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setHolderEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfHolderEncryptionKey] = value;
return *this;
}
/**
* @brief Build and return the completed MPToken wrapper.
* @param index The ledger entry index.
@@ -302,6 +302,78 @@ public:
{
return this->sle_->isFieldPresent(sfReferenceHolding);
}
/**
* @brief Get sfIssuerEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getIssuerEncryptionKey() const
{
if (hasIssuerEncryptionKey())
return this->sle_->at(sfIssuerEncryptionKey);
return std::nullopt;
}
/**
* @brief Check if sfIssuerEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasIssuerEncryptionKey() const
{
return this->sle_->isFieldPresent(sfIssuerEncryptionKey);
}
/**
* @brief Get sfAuditorEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getAuditorEncryptionKey() const
{
if (hasAuditorEncryptionKey())
return this->sle_->at(sfAuditorEncryptionKey);
return std::nullopt;
}
/**
* @brief Check if sfAuditorEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorEncryptionKey() const
{
return this->sle_->isFieldPresent(sfAuditorEncryptionKey);
}
/**
* @brief Get sfConfidentialOutstandingAmount (SoeDefault)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT64::type::value_type>
getConfidentialOutstandingAmount() const
{
if (hasConfidentialOutstandingAmount())
return this->sle_->at(sfConfidentialOutstandingAmount);
return std::nullopt;
}
/**
* @brief Check if sfConfidentialOutstandingAmount is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialOutstandingAmount() const
{
return this->sle_->isFieldPresent(sfConfidentialOutstandingAmount);
}
};
/**
@@ -504,6 +576,39 @@ public:
return *this;
}
/**
* @brief Set sfIssuerEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceBuilder&
setIssuerEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfIssuerEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfAuditorEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceBuilder&
setAuditorEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfAuditorEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfConfidentialOutstandingAmount (SoeDefault)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceBuilder&
setConfidentialOutstandingAmount(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfConfidentialOutstandingAmount] = value;
return *this;
}
/**
* @brief Build and return the completed MPTokenIssuance wrapper.
* @param index The ledger entry index.
@@ -0,0 +1,201 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class ConfidentialMPTClawbackBuilder;
/**
* @brief Transaction: ConfidentialMPTClawback
*
* Type: ttCONFIDENTIAL_MPT_CLAWBACK (89)
* Delegable: Delegation::Delegable
* Amendment: featureConfidentialTransfer
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTClawbackBuilder to construct new transactions.
*/
class ConfidentialMPTClawback : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_CLAWBACK;
/**
* @brief Construct a ConfidentialMPTClawback transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit ConfidentialMPTClawback(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTClawback");
}
}
// Transaction-specific field getters
/**
* @brief Get sfMPTokenIssuanceID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT192::type::value_type
getMPTokenIssuanceID() const
{
return this->tx_->at(sfMPTokenIssuanceID);
}
/**
* @brief Get sfHolder (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_ACCOUNT::type::value_type
getHolder() const
{
return this->tx_->at(sfHolder);
}
/**
* @brief Get sfMPTAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT64::type::value_type
getMPTAmount() const
{
return this->tx_->at(sfMPTAmount);
}
/**
* @brief Get sfZKProof (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getZKProof() const
{
return this->tx_->at(sfZKProof);
}
};
/**
* @brief Builder for ConfidentialMPTClawback transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class ConfidentialMPTClawbackBuilder : public TransactionBuilderBase<ConfidentialMPTClawbackBuilder>
{
public:
/**
* @brief Construct a new ConfidentialMPTClawbackBuilder with required fields.
* @param account The account initiating the transaction.
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
* @param holder The sfHolder field value.
* @param mPTAmount The sfMPTAmount field value.
* @param zKProof The sfZKProof field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
ConfidentialMPTClawbackBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::decay_t<typename SF_ACCOUNT::type::value_type> const& holder, std::decay_t<typename SF_UINT64::type::value_type> const& mPTAmount, std::decay_t<typename SF_VL::type::value_type> const& zKProof, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<ConfidentialMPTClawbackBuilder>(ttCONFIDENTIAL_MPT_CLAWBACK, account, sequence, fee)
{
setMPTokenIssuanceID(mPTokenIssuanceID);
setHolder(holder);
setMPTAmount(mPTAmount);
setZKProof(zKProof);
}
/**
* @brief Construct a ConfidentialMPTClawbackBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
ConfidentialMPTClawbackBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_CLAWBACK)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTClawbackBuilder");
}
object_ = *tx;
}
/** @brief Transaction-specific field setters */
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTClawbackBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfHolder (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTClawbackBuilder&
setHolder(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfHolder] = value;
return *this;
}
/**
* @brief Set sfMPTAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTClawbackBuilder&
setMPTAmount(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfMPTAmount] = value;
return *this;
}
/**
* @brief Set sfZKProof (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTClawbackBuilder&
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfZKProof] = value;
return *this;
}
/**
* @brief Build and return the ConfidentialMPTClawback wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
ConfidentialMPTClawback
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return ConfidentialMPTClawback{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
@@ -0,0 +1,336 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class ConfidentialMPTConvertBuilder;
/**
* @brief Transaction: ConfidentialMPTConvert
*
* Type: ttCONFIDENTIAL_MPT_CONVERT (85)
* Delegable: Delegation::Delegable
* Amendment: featureConfidentialTransfer
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTConvertBuilder to construct new transactions.
*/
class ConfidentialMPTConvert : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_CONVERT;
/**
* @brief Construct a ConfidentialMPTConvert transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit ConfidentialMPTConvert(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvert");
}
}
// Transaction-specific field getters
/**
* @brief Get sfMPTokenIssuanceID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT192::type::value_type
getMPTokenIssuanceID() const
{
return this->tx_->at(sfMPTokenIssuanceID);
}
/**
* @brief Get sfMPTAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT64::type::value_type
getMPTAmount() const
{
return this->tx_->at(sfMPTAmount);
}
/**
* @brief Get sfHolderEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getHolderEncryptionKey() const
{
if (hasHolderEncryptionKey())
{
return this->tx_->at(sfHolderEncryptionKey);
}
return std::nullopt;
}
/**
* @brief Check if sfHolderEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasHolderEncryptionKey() const
{
return this->tx_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfHolderEncryptedAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getHolderEncryptedAmount() const
{
return this->tx_->at(sfHolderEncryptedAmount);
}
/**
* @brief Get sfIssuerEncryptedAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getIssuerEncryptedAmount() const
{
return this->tx_->at(sfIssuerEncryptedAmount);
}
/**
* @brief Get sfAuditorEncryptedAmount (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getAuditorEncryptedAmount() const
{
if (hasAuditorEncryptedAmount())
{
return this->tx_->at(sfAuditorEncryptedAmount);
}
return std::nullopt;
}
/**
* @brief Check if sfAuditorEncryptedAmount is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorEncryptedAmount() const
{
return this->tx_->isFieldPresent(sfAuditorEncryptedAmount);
}
/**
* @brief Get sfBlindingFactor (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT256::type::value_type
getBlindingFactor() const
{
return this->tx_->at(sfBlindingFactor);
}
/**
* @brief Get sfZKProof (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getZKProof() const
{
if (hasZKProof())
{
return this->tx_->at(sfZKProof);
}
return std::nullopt;
}
/**
* @brief Check if sfZKProof is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasZKProof() const
{
return this->tx_->isFieldPresent(sfZKProof);
}
};
/**
* @brief Builder for ConfidentialMPTConvert transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class ConfidentialMPTConvertBuilder : public TransactionBuilderBase<ConfidentialMPTConvertBuilder>
{
public:
/**
* @brief Construct a new ConfidentialMPTConvertBuilder with required fields.
* @param account The account initiating the transaction.
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
* @param mPTAmount The sfMPTAmount field value.
* @param holderEncryptedAmount The sfHolderEncryptedAmount field value.
* @param issuerEncryptedAmount The sfIssuerEncryptedAmount field value.
* @param blindingFactor The sfBlindingFactor field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
ConfidentialMPTConvertBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::decay_t<typename SF_UINT64::type::value_type> const& mPTAmount, std::decay_t<typename SF_VL::type::value_type> const& holderEncryptedAmount, std::decay_t<typename SF_VL::type::value_type> const& issuerEncryptedAmount, std::decay_t<typename SF_UINT256::type::value_type> const& blindingFactor, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<ConfidentialMPTConvertBuilder>(ttCONFIDENTIAL_MPT_CONVERT, account, sequence, fee)
{
setMPTokenIssuanceID(mPTokenIssuanceID);
setMPTAmount(mPTAmount);
setHolderEncryptedAmount(holderEncryptedAmount);
setIssuerEncryptedAmount(issuerEncryptedAmount);
setBlindingFactor(blindingFactor);
}
/**
* @brief Construct a ConfidentialMPTConvertBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
ConfidentialMPTConvertBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_CONVERT)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvertBuilder");
}
object_ = *tx;
}
/** @brief Transaction-specific field setters */
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfMPTAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBuilder&
setMPTAmount(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfMPTAmount] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBuilder&
setHolderEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfHolderEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptedAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBuilder&
setHolderEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfHolderEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfIssuerEncryptedAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBuilder&
setIssuerEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfIssuerEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfAuditorEncryptedAmount (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBuilder&
setAuditorEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfAuditorEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfBlindingFactor (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBuilder&
setBlindingFactor(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfBlindingFactor] = value;
return *this;
}
/**
* @brief Set sfZKProof (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBuilder&
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfZKProof] = value;
return *this;
}
/**
* @brief Build and return the ConfidentialMPTConvert wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
ConfidentialMPTConvert
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return ConfidentialMPTConvert{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
@@ -0,0 +1,310 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class ConfidentialMPTConvertBackBuilder;
/**
* @brief Transaction: ConfidentialMPTConvertBack
*
* Type: ttCONFIDENTIAL_MPT_CONVERT_BACK (87)
* Delegable: Delegation::Delegable
* Amendment: featureConfidentialTransfer
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTConvertBackBuilder to construct new transactions.
*/
class ConfidentialMPTConvertBack : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_CONVERT_BACK;
/**
* @brief Construct a ConfidentialMPTConvertBack transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit ConfidentialMPTConvertBack(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvertBack");
}
}
// Transaction-specific field getters
/**
* @brief Get sfMPTokenIssuanceID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT192::type::value_type
getMPTokenIssuanceID() const
{
return this->tx_->at(sfMPTokenIssuanceID);
}
/**
* @brief Get sfMPTAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT64::type::value_type
getMPTAmount() const
{
return this->tx_->at(sfMPTAmount);
}
/**
* @brief Get sfHolderEncryptedAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getHolderEncryptedAmount() const
{
return this->tx_->at(sfHolderEncryptedAmount);
}
/**
* @brief Get sfIssuerEncryptedAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getIssuerEncryptedAmount() const
{
return this->tx_->at(sfIssuerEncryptedAmount);
}
/**
* @brief Get sfAuditorEncryptedAmount (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getAuditorEncryptedAmount() const
{
if (hasAuditorEncryptedAmount())
{
return this->tx_->at(sfAuditorEncryptedAmount);
}
return std::nullopt;
}
/**
* @brief Check if sfAuditorEncryptedAmount is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorEncryptedAmount() const
{
return this->tx_->isFieldPresent(sfAuditorEncryptedAmount);
}
/**
* @brief Get sfBlindingFactor (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT256::type::value_type
getBlindingFactor() const
{
return this->tx_->at(sfBlindingFactor);
}
/**
* @brief Get sfZKProof (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getZKProof() const
{
return this->tx_->at(sfZKProof);
}
/**
* @brief Get sfBalanceCommitment (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getBalanceCommitment() const
{
return this->tx_->at(sfBalanceCommitment);
}
};
/**
* @brief Builder for ConfidentialMPTConvertBack transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class ConfidentialMPTConvertBackBuilder : public TransactionBuilderBase<ConfidentialMPTConvertBackBuilder>
{
public:
/**
* @brief Construct a new ConfidentialMPTConvertBackBuilder with required fields.
* @param account The account initiating the transaction.
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
* @param mPTAmount The sfMPTAmount field value.
* @param holderEncryptedAmount The sfHolderEncryptedAmount field value.
* @param issuerEncryptedAmount The sfIssuerEncryptedAmount field value.
* @param blindingFactor The sfBlindingFactor field value.
* @param zKProof The sfZKProof field value.
* @param balanceCommitment The sfBalanceCommitment field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
ConfidentialMPTConvertBackBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::decay_t<typename SF_UINT64::type::value_type> const& mPTAmount, std::decay_t<typename SF_VL::type::value_type> const& holderEncryptedAmount, std::decay_t<typename SF_VL::type::value_type> const& issuerEncryptedAmount, std::decay_t<typename SF_UINT256::type::value_type> const& blindingFactor, std::decay_t<typename SF_VL::type::value_type> const& zKProof, std::decay_t<typename SF_VL::type::value_type> const& balanceCommitment, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<ConfidentialMPTConvertBackBuilder>(ttCONFIDENTIAL_MPT_CONVERT_BACK, account, sequence, fee)
{
setMPTokenIssuanceID(mPTokenIssuanceID);
setMPTAmount(mPTAmount);
setHolderEncryptedAmount(holderEncryptedAmount);
setIssuerEncryptedAmount(issuerEncryptedAmount);
setBlindingFactor(blindingFactor);
setZKProof(zKProof);
setBalanceCommitment(balanceCommitment);
}
/**
* @brief Construct a ConfidentialMPTConvertBackBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
ConfidentialMPTConvertBackBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_CONVERT_BACK)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvertBackBuilder");
}
object_ = *tx;
}
/** @brief Transaction-specific field setters */
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBackBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfMPTAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBackBuilder&
setMPTAmount(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfMPTAmount] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptedAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBackBuilder&
setHolderEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfHolderEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfIssuerEncryptedAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBackBuilder&
setIssuerEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfIssuerEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfAuditorEncryptedAmount (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBackBuilder&
setAuditorEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfAuditorEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfBlindingFactor (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBackBuilder&
setBlindingFactor(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfBlindingFactor] = value;
return *this;
}
/**
* @brief Set sfZKProof (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBackBuilder&
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfZKProof] = value;
return *this;
}
/**
* @brief Set sfBalanceCommitment (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTConvertBackBuilder&
setBalanceCommitment(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfBalanceCommitment] = value;
return *this;
}
/**
* @brief Build and return the ConfidentialMPTConvertBack wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
ConfidentialMPTConvertBack
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return ConfidentialMPTConvertBack{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
@@ -0,0 +1,129 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class ConfidentialMPTMergeInboxBuilder;
/**
* @brief Transaction: ConfidentialMPTMergeInbox
*
* Type: ttCONFIDENTIAL_MPT_MERGE_INBOX (86)
* Delegable: Delegation::Delegable
* Amendment: featureConfidentialTransfer
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTMergeInboxBuilder to construct new transactions.
*/
class ConfidentialMPTMergeInbox : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_MERGE_INBOX;
/**
* @brief Construct a ConfidentialMPTMergeInbox transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit ConfidentialMPTMergeInbox(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTMergeInbox");
}
}
// Transaction-specific field getters
/**
* @brief Get sfMPTokenIssuanceID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT192::type::value_type
getMPTokenIssuanceID() const
{
return this->tx_->at(sfMPTokenIssuanceID);
}
};
/**
* @brief Builder for ConfidentialMPTMergeInbox transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class ConfidentialMPTMergeInboxBuilder : public TransactionBuilderBase<ConfidentialMPTMergeInboxBuilder>
{
public:
/**
* @brief Construct a new ConfidentialMPTMergeInboxBuilder with required fields.
* @param account The account initiating the transaction.
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
ConfidentialMPTMergeInboxBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<ConfidentialMPTMergeInboxBuilder>(ttCONFIDENTIAL_MPT_MERGE_INBOX, account, sequence, fee)
{
setMPTokenIssuanceID(mPTokenIssuanceID);
}
/**
* @brief Construct a ConfidentialMPTMergeInboxBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
ConfidentialMPTMergeInboxBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_MERGE_INBOX)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTMergeInboxBuilder");
}
object_ = *tx;
}
/** @brief Transaction-specific field setters */
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTMergeInboxBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Build and return the ConfidentialMPTMergeInbox wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
ConfidentialMPTMergeInbox
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return ConfidentialMPTMergeInbox{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
@@ -0,0 +1,408 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class ConfidentialMPTSendBuilder;
/**
* @brief Transaction: ConfidentialMPTSend
*
* Type: ttCONFIDENTIAL_MPT_SEND (88)
* Delegable: Delegation::Delegable
* Amendment: featureConfidentialTransfer
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTSendBuilder to construct new transactions.
*/
class ConfidentialMPTSend : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_SEND;
/**
* @brief Construct a ConfidentialMPTSend transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit ConfidentialMPTSend(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTSend");
}
}
// Transaction-specific field getters
/**
* @brief Get sfMPTokenIssuanceID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT192::type::value_type
getMPTokenIssuanceID() const
{
return this->tx_->at(sfMPTokenIssuanceID);
}
/**
* @brief Get sfDestination (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_ACCOUNT::type::value_type
getDestination() const
{
return this->tx_->at(sfDestination);
}
/**
* @brief Get sfDestinationTag (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getDestinationTag() const
{
if (hasDestinationTag())
{
return this->tx_->at(sfDestinationTag);
}
return std::nullopt;
}
/**
* @brief Check if sfDestinationTag is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasDestinationTag() const
{
return this->tx_->isFieldPresent(sfDestinationTag);
}
/**
* @brief Get sfSenderEncryptedAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getSenderEncryptedAmount() const
{
return this->tx_->at(sfSenderEncryptedAmount);
}
/**
* @brief Get sfDestinationEncryptedAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getDestinationEncryptedAmount() const
{
return this->tx_->at(sfDestinationEncryptedAmount);
}
/**
* @brief Get sfIssuerEncryptedAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getIssuerEncryptedAmount() const
{
return this->tx_->at(sfIssuerEncryptedAmount);
}
/**
* @brief Get sfAuditorEncryptedAmount (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getAuditorEncryptedAmount() const
{
if (hasAuditorEncryptedAmount())
{
return this->tx_->at(sfAuditorEncryptedAmount);
}
return std::nullopt;
}
/**
* @brief Check if sfAuditorEncryptedAmount is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorEncryptedAmount() const
{
return this->tx_->isFieldPresent(sfAuditorEncryptedAmount);
}
/**
* @brief Get sfZKProof (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getZKProof() const
{
return this->tx_->at(sfZKProof);
}
/**
* @brief Get sfAmountCommitment (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getAmountCommitment() const
{
return this->tx_->at(sfAmountCommitment);
}
/**
* @brief Get sfBalanceCommitment (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_VL::type::value_type
getBalanceCommitment() const
{
return this->tx_->at(sfBalanceCommitment);
}
/**
* @brief Get sfCredentialIDs (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VECTOR256::type::value_type>
getCredentialIDs() const
{
if (hasCredentialIDs())
{
return this->tx_->at(sfCredentialIDs);
}
return std::nullopt;
}
/**
* @brief Check if sfCredentialIDs is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasCredentialIDs() const
{
return this->tx_->isFieldPresent(sfCredentialIDs);
}
};
/**
* @brief Builder for ConfidentialMPTSend transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class ConfidentialMPTSendBuilder : public TransactionBuilderBase<ConfidentialMPTSendBuilder>
{
public:
/**
* @brief Construct a new ConfidentialMPTSendBuilder with required fields.
* @param account The account initiating the transaction.
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
* @param destination The sfDestination field value.
* @param senderEncryptedAmount The sfSenderEncryptedAmount field value.
* @param destinationEncryptedAmount The sfDestinationEncryptedAmount field value.
* @param issuerEncryptedAmount The sfIssuerEncryptedAmount field value.
* @param zKProof The sfZKProof field value.
* @param amountCommitment The sfAmountCommitment field value.
* @param balanceCommitment The sfBalanceCommitment field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
ConfidentialMPTSendBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::decay_t<typename SF_ACCOUNT::type::value_type> const& destination, std::decay_t<typename SF_VL::type::value_type> const& senderEncryptedAmount, std::decay_t<typename SF_VL::type::value_type> const& destinationEncryptedAmount, std::decay_t<typename SF_VL::type::value_type> const& issuerEncryptedAmount, std::decay_t<typename SF_VL::type::value_type> const& zKProof, std::decay_t<typename SF_VL::type::value_type> const& amountCommitment, std::decay_t<typename SF_VL::type::value_type> const& balanceCommitment, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<ConfidentialMPTSendBuilder>(ttCONFIDENTIAL_MPT_SEND, account, sequence, fee)
{
setMPTokenIssuanceID(mPTokenIssuanceID);
setDestination(destination);
setSenderEncryptedAmount(senderEncryptedAmount);
setDestinationEncryptedAmount(destinationEncryptedAmount);
setIssuerEncryptedAmount(issuerEncryptedAmount);
setZKProof(zKProof);
setAmountCommitment(amountCommitment);
setBalanceCommitment(balanceCommitment);
}
/**
* @brief Construct a ConfidentialMPTSendBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
ConfidentialMPTSendBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_SEND)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTSendBuilder");
}
object_ = *tx;
}
/** @brief Transaction-specific field setters */
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfDestination (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setDestination(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfDestination] = value;
return *this;
}
/**
* @brief Set sfDestinationTag (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfDestinationTag] = value;
return *this;
}
/**
* @brief Set sfSenderEncryptedAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setSenderEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfSenderEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfDestinationEncryptedAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setDestinationEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfDestinationEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfIssuerEncryptedAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setIssuerEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfIssuerEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfAuditorEncryptedAmount (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setAuditorEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfAuditorEncryptedAmount] = value;
return *this;
}
/**
* @brief Set sfZKProof (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfZKProof] = value;
return *this;
}
/**
* @brief Set sfAmountCommitment (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setAmountCommitment(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfAmountCommitment] = value;
return *this;
}
/**
* @brief Set sfBalanceCommitment (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setBalanceCommitment(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfBalanceCommitment] = value;
return *this;
}
/**
* @brief Set sfCredentialIDs (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTSendBuilder&
setCredentialIDs(std::decay_t<typename SF_VECTOR256::type::value_type> const& value)
{
object_[sfCredentialIDs] = value;
return *this;
}
/**
* @brief Build and return the ConfidentialMPTSend wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
ConfidentialMPTSend
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return ConfidentialMPTSend{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
@@ -187,6 +187,58 @@ public:
{
return this->tx_->isFieldPresent(sfMutableFlags);
}
/**
* @brief Get sfIssuerEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getIssuerEncryptionKey() const
{
if (hasIssuerEncryptionKey())
{
return this->tx_->at(sfIssuerEncryptionKey);
}
return std::nullopt;
}
/**
* @brief Check if sfIssuerEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasIssuerEncryptionKey() const
{
return this->tx_->isFieldPresent(sfIssuerEncryptionKey);
}
/**
* @brief Get sfAuditorEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getAuditorEncryptionKey() const
{
if (hasAuditorEncryptionKey())
{
return this->tx_->at(sfAuditorEncryptionKey);
}
return std::nullopt;
}
/**
* @brief Check if sfAuditorEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorEncryptionKey() const
{
return this->tx_->isFieldPresent(sfAuditorEncryptionKey);
}
};
/**
@@ -297,6 +349,28 @@ public:
return *this;
}
/**
* @brief Set sfIssuerEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceSetBuilder&
setIssuerEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfIssuerEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfAuditorEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceSetBuilder&
setAuditorEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfAuditorEncryptionKey] = value;
return *this;
}
/**
* @brief Build and return the MPTokenIssuanceSet wrapper.
* @param publicKey The public key for signing.
+5
View File
@@ -7,6 +7,7 @@
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/applySteps.h>
#include <cstdint>
#include <tuple>
#include <utility>
@@ -186,6 +187,10 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
// Returns the base fee plus extra base fee units, not scaled for load.
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx, std::uint32_t extraBaseFeeMultiplier);
/* Do NOT define an invokePreflight function in a derived class.
Instead, define:
@@ -413,6 +413,7 @@ using InvariantChecks = std::tuple<
ValidLoanBroker,
ValidLoan,
ValidVault,
ValidConfidentialMPToken,
ValidMPTPayment,
ValidAmounts,
ValidMPTTransfer>;
+160 -11
View File
@@ -36,17 +36,42 @@ class ValidMPTIssuance
std::vector<std::shared_ptr<SLE const>> deletedHoldings_;
public:
/**
* @brief Track MPT issuance and holding creations, deletions, and
* mutations.
*
* @param isDelete Whether the ledger entry is being deleted.
* @param before The ledger entry before transaction application.
* @param after The ledger entry after transaction application.
*/
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after);
/**
* @brief Verify MPT issuance invariants after transaction application.
*
* @param tx The transaction being checked.
* @param result The transaction result code.
* @param fee The fee charged by the transaction.
* @param view The ledger view after transaction application.
* @param j Journal used for diagnostics.
* @return true if the invariant checks pass, otherwise false.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
finalize(
STTx const& tx,
TER const result,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j) const;
};
/** Verify:
* - OutstandingAmount <= MaximumAmount for any MPT
* - OutstandingAmount after = OutstandingAmount before +
* sum (MPT after - MPT before) - this is total MPT credit/debit
/**
* @brief Verify public MPT amount and outstanding amount accounting.
*
* Checks that OutstandingAmount does not exceed MaximumAmount and that
* OutstandingAmount after application equals OutstandingAmount before
* application plus the net holder balance delta.
*/
class ValidMPTPayment
{
@@ -64,11 +89,104 @@ class ValidMPTPayment
hash_map<uint192, MPTData> data_;
public:
/**
* @brief Track MPT amount and outstanding amount changes.
*
* @param isDelete Whether the ledger entry is being deleted.
* @param before The ledger entry before transaction application.
* @param after The ledger entry after transaction application.
*/
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after);
/**
* @brief Verify public MPT payment accounting invariants.
*
* @param tx The transaction being checked.
* @param result The transaction result code.
* @param fee The fee charged by the transaction.
* @param view The ledger view after transaction application.
* @param j Journal used for diagnostics.
* @return true if the invariant checks pass, otherwise false.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
finalize(
STTx const& tx,
TER const result,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j);
};
/**
* @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 confidential balance fields can only be created or changed if
* lsfMPTCanHoldConfidentialBalance is set on the issuance.
* - Encrypted field existence consistency:
* If sfConfidentialBalanceSpending/sfConfidentialBalanceInbox exists, then
* sfIssuerEncryptedBalance must also exist (and vice versa). If
* sfAuditorEncryptedBalance exists, then those core encrypted balance fields
* must also exist.
* - 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 changesConfidentialFields = false;
bool badVersion = false;
};
std::map<uint192, Changes> changes_;
public:
/**
* @brief Track confidential MPT balance, issuance, and version changes.
*
* @param isDelete Whether the ledger entry is being deleted.
* @param before The ledger entry before transaction application.
* @param after The ledger entry after transaction application.
*/
void
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
/**
* @brief Verify confidential MPT accounting and encrypted-field
* invariants.
*
* @param tx The transaction being checked.
* @param result The transaction result code.
* @param fee The fee charged by the transaction.
* @param view The ledger view after transaction application.
* @param j Journal used for diagnostics.
* @return true if the invariant checks pass, otherwise false.
*/
bool
finalize(
STTx const& tx,
TER const result,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j);
};
class ValidMPTTransfer
@@ -85,11 +203,36 @@ class ValidMPTTransfer
hash_map<uint256, bool> deletedAuthorized_;
public:
/**
* @brief Track MPT balance changes and deleted authorization state.
*
* @param isDelete Whether the ledger entry is being deleted.
* @param before The ledger entry before transaction application.
* @param after The ledger entry after transaction application.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
/**
* @brief Verify MPT transfer authorization invariants.
*
* @param tx The transaction being checked.
* @param result The transaction result code.
* @param fee The fee charged by the transaction.
* @param view The ledger view after transaction application.
* @param j Journal used for diagnostics.
* @return true if the invariant checks pass, otherwise false.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
finalize(
STTx const& tx,
TER const result,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j);
private:
/**
@@ -99,7 +242,13 @@ private:
* finalize() runs, so their authorization state is captured during
* visitEntry() and stored in deletedAuthorized_. For deleted MPTokens,
* returns true if reqAuth is false or lsfMPTAuthorized was set at deletion.
* For existing MPTokens, returns the result of requireAuth()
* For existing MPTokens, returns the result of requireAuth().
*
* @param view The ledger view after transaction application.
* @param mptid The MPToken issuance ID.
* @param holder The holder account being checked.
* @param requireAuth Whether the issuance requires explicit authorization.
* @return true if the holder is authorized, otherwise false.
*/
[[nodiscard]] bool
isAuthorized(
@@ -0,0 +1,59 @@
#pragma once
#include <xrpl/tx/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 auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit ConfidentialMPTClawback(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl
@@ -0,0 +1,61 @@
#pragma once
#include <xrpl/tx/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 auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit ConfidentialMPTConvert(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl
@@ -0,0 +1,62 @@
#pragma once
#include <xrpl/tx/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 auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit ConfidentialMPTConvertBack(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl
@@ -0,0 +1,63 @@
#pragma once
#include <xrpl/tx/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 auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit ConfidentialMPTMergeInbox(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl
@@ -0,0 +1,72 @@
#pragma once
#include <xrpl/tx/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 auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit ConfidentialMPTSend(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl
@@ -346,9 +346,9 @@ verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, b
}
TER
verifyDepositPreauth(
checkDepositPreauth(
STTx const& tx,
ApplyView& view,
ReadView const& view,
AccountID const& src,
AccountID const& dst,
SLE::const_ref sleDst,
@@ -360,9 +360,27 @@ verifyDepositPreauth(
// 2. If src is deposit preauthorized by dst (either by account or by
// credentials).
bool const credentialsPresent = tx.isFieldPresent(sfCredentialIDs);
if (sleDst && ((sleDst->getFlags() & lsfDepositAuth) != 0u))
{
if (src != dst)
{
if (!view.exists(keylet::depositPreauth(dst, src)))
{
return !tx.isFieldPresent(sfCredentialIDs)
? tecNO_PERMISSION
: credentials::authorizedDepositPreauth(
view, tx.getFieldV256(sfCredentialIDs), dst);
}
}
}
if (credentialsPresent)
return tesSUCCESS;
}
TER
cleanupExpiredCredentials(STTx const& tx, ApplyView& view, beast::Journal j)
{
if (tx.isFieldPresent(sfCredentialIDs))
{
auto const foundExpired =
credentials::removeExpired(view, tx.getFieldV256(sfCredentialIDs), j);
@@ -372,20 +390,22 @@ verifyDepositPreauth(
return tecEXPIRED;
}
if (sleDst && sleDst->isFlag(lsfDepositAuth))
{
if (src != dst)
{
if (!view.exists(keylet::depositPreauth(dst, src)))
{
return !credentialsPresent ? tecNO_PERMISSION
: credentials::authorizedDepositPreauth(
view, tx.getFieldV256(sfCredentialIDs), dst);
}
}
}
return tesSUCCESS;
}
TER
verifyDepositPreauth(
STTx const& tx,
ApplyView& view,
AccountID const& src,
AccountID const& dst,
SLE::const_ref sleDst,
beast::Journal j)
{
if (auto const err = cleanupExpiredCredentials(tx, view, j); !isTesSuccess(err))
return err;
return checkDepositPreauth(tx, view, src, dst, sleDst, j);
}
} // namespace xrpl
@@ -290,6 +290,15 @@ removeEmptyHolding(
(view.rules().enabled(fixCleanup3_1_3) && (*mptoken)[~sfLockedAmount].valueOr(0) != 0))
return tecHAS_OBLIGATIONS;
// Don't delete if the token still has confidential balances
if (mptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
mptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
mptoken->isFieldPresent(sfIssuerEncryptedBalance) ||
mptoken->isFieldPresent(sfAuditorEncryptedBalance))
{
return tecHAS_OBLIGATIONS;
}
return authorizeMPToken(
view,
{}, // priorBalance
+2 -1
View File
@@ -349,7 +349,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 == AuthHandling::ZeroIfUnauthorized &&
view.rules().enabled(featureSingleAssetVault))
(view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureConfidentialTransfer)))
{
if (auto const err = requireAuth(view, mptIssue, account, AuthType::StrongAuth);
!isTesSuccess(err))
@@ -0,0 +1,487 @@
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <openssl/rand.h>
#include <utility/mpt_utility.h>
#include <mpt_protocol.h>
#include <secp256k1.h>
#include <secp256k1_mpt.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <optional>
#include <stdexcept>
#include <vector>
namespace xrpl {
namespace {
account_id
toAccountId(AccountID const& account)
{
account_id res;
std::memcpy(res.bytes, account.data(), kMPT_ACCOUNT_ID_SIZE);
return res;
}
mpt_issuance_id
toIssuanceId(uint192 const& issuance)
{
mpt_issuance_id res;
std::memcpy(res.bytes, issuance.data(), kMPT_ISSUANCE_ID_SIZE);
return res;
}
/**
* @brief Pack a ConfidentialRecipient (public key + ElGamal ciphertext) into the
* secp256k1-mpt participant struct. Callers MUST have already validated that
* r.publicKey.size() == kEcPubKeyLength and
* r.encryptedAmount.size() == kEcGamalEncryptedTotalLength;
*/
mpt_confidential_participant
toParticipant(ConfidentialRecipient const& r)
{
mpt_confidential_participant p{};
std::memcpy(p.pubkey, r.publicKey.data(), kEcPubKeyLength);
std::memcpy(p.ciphertext, r.encryptedAmount.data(), kEcGamalEncryptedTotalLength);
return p;
}
} // namespace
uint256
getSendContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
AccountID const& destination,
std::uint32_t version)
{
uint256 result;
mpt_get_send_context_hash(
toAccountId(account),
toIssuanceId(issuanceID),
sequence,
toAccountId(destination),
version,
result.data());
return result;
}
uint256
getClawbackContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
AccountID const& holder)
{
uint256 result;
mpt_get_clawback_context_hash(
toAccountId(account),
toIssuanceId(issuanceID),
sequence,
toAccountId(holder),
result.data());
return result;
}
uint256
getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence)
{
uint256 result;
mpt_get_convert_context_hash(
toAccountId(account), toIssuanceId(issuanceID), sequence, result.data());
return result;
}
uint256
getConvertBackContextHash(
AccountID const& account,
uint192 const& issuanceID,
std::uint32_t sequence,
std::uint32_t version)
{
uint256 result;
mpt_get_convert_back_context_hash(
toAccountId(account), toIssuanceId(issuanceID), sequence, version, result.data());
return result;
}
std::optional<EcPair>
makeEcPair(Slice const& buffer)
{
if (buffer.length() != 2 * kEcCiphertextComponentLength)
return std::nullopt; // LCOV_EXCL_LINE
auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) {
return secp256k1_ec_pubkey_parse(secp256k1Context(), &out, slice.data(), slice.length());
};
Slice const s1{buffer.data(), kEcCiphertextComponentLength};
Slice const s2{buffer.data() + kEcCiphertextComponentLength, kEcCiphertextComponentLength};
EcPair pair{};
if (parsePubKey(s1, pair.c1) != 1 || parsePubKey(s2, pair.c2) != 1)
return std::nullopt;
return pair;
}
std::optional<Buffer>
serializeEcPair(EcPair const& pair)
{
auto serializePubKey = [](secp256k1_pubkey const& pub, unsigned char* out) {
size_t outLen = kEcCiphertextComponentLength; // 33 bytes
auto const ret = secp256k1_ec_pubkey_serialize(
secp256k1Context(), out, &outLen, &pub, SECP256K1_EC_COMPRESSED);
return ret == 1 && outLen == kEcCiphertextComponentLength;
};
Buffer buffer(kEcGamalEncryptedTotalLength);
auto const ptr = buffer.data();
bool const res1 = serializePubKey(pair.c1, ptr);
bool const res2 = serializePubKey(pair.c2, ptr + kEcCiphertextComponentLength);
if (!res1 || !res2)
return std::nullopt;
return buffer;
}
bool
isValidCiphertext(Slice const& buffer)
{
return makeEcPair(buffer).has_value();
}
bool
isValidCompressedECPoint(Slice const& buffer)
{
if (buffer.size() != kCompressedEcPointLength)
return false;
// Compressed EC points must start with 0x02 or 0x03
if (buffer[0] != kEcCompressedPrefixEvenY && buffer[0] != kEcCompressedPrefixOddY)
return false;
secp256k1_pubkey point;
return secp256k1_ec_pubkey_parse(secp256k1Context(), &point, buffer.data(), buffer.size()) == 1;
}
std::optional<Buffer>
homomorphicAdd(Slice const& a, Slice const& b)
{
if (a.length() != kEcGamalEncryptedTotalLength || b.length() != kEcGamalEncryptedTotalLength)
return std::nullopt;
auto const pairA = makeEcPair(a);
auto const pairB = makeEcPair(b);
if (!pairA || !pairB)
return std::nullopt;
EcPair sum{};
if (auto res = secp256k1_elgamal_add(
secp256k1Context(), &sum.c1, &sum.c2, &pairA->c1, &pairA->c2, &pairB->c1, &pairB->c2);
res != 1)
{
return std::nullopt;
}
return serializeEcPair(sum);
}
std::optional<Buffer>
homomorphicSubtract(Slice const& a, Slice const& b)
{
if (a.length() != kEcGamalEncryptedTotalLength || b.length() != kEcGamalEncryptedTotalLength)
return std::nullopt;
auto const pairA = makeEcPair(a);
auto const pairB = makeEcPair(b);
if (!pairA || !pairB)
return std::nullopt;
EcPair diff{};
if (auto const res = secp256k1_elgamal_subtract(
secp256k1Context(), &diff.c1, &diff.c2, &pairA->c1, &pairA->c2, &pairB->c1, &pairB->c2);
res != 1)
{
return std::nullopt;
}
return serializeEcPair(diff);
}
std::optional<Buffer>
rerandomizeCiphertext(Slice const& ciphertext, Slice const& pubKeySlice, Slice const& randomness)
{
auto zero = encryptAmount(0, pubKeySlice, randomness);
if (!zero)
return std::nullopt;
return homomorphicAdd(ciphertext, *zero);
}
Buffer
generateBlindingFactor()
{
unsigned char blindingFactor[kEcBlindingFactorLength];
// todo: might need to be updated using another RNG
if (RAND_bytes(blindingFactor, kEcBlindingFactorLength) != 1)
Throw<std::runtime_error>("Failed to generate random number");
return Buffer(blindingFactor, kEcBlindingFactorLength);
}
std::optional<Buffer>
encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor)
{
if (blindingFactor.size() != kEcBlindingFactorLength || pubKeySlice.size() != kEcPubKeyLength)
return std::nullopt;
Buffer out(kEcGamalEncryptedTotalLength);
if (mpt_encrypt_amount(amt, pubKeySlice.data(), blindingFactor.data(), out.data()) != 0)
return std::nullopt;
return out;
}
std::optional<Buffer>
encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId)
{
if (pubKeySlice.size() != kEcPubKeyLength)
return std::nullopt; // LCOV_EXCL_LINE
EcPair pair{};
secp256k1_pubkey pubKey;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pubKey, pubKeySlice.data(), kEcPubKeyLength);
res != 1)
{
return std::nullopt; // LCOV_EXCL_LINE
}
if (auto res = generate_canonical_encrypted_zero(
secp256k1Context(), &pair.c1, &pair.c2, &pubKey, account.data(), mptId.data());
res != 1)
{
return std::nullopt; // LCOV_EXCL_LINE
}
return serializeEcPair(pair);
}
TER
verifyRevealedAmount(
uint64_t const amount,
Slice const& blindingFactor,
ConfidentialRecipient const& holder,
ConfidentialRecipient const& issuer,
std::optional<ConfidentialRecipient> const& auditor)
{
if (blindingFactor.size() != kEcBlindingFactorLength ||
holder.publicKey.size() != kEcPubKeyLength ||
holder.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
issuer.publicKey.size() != kEcPubKeyLength ||
issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
auto const holderP = toParticipant(holder);
auto const issuerP = toParticipant(issuer);
mpt_confidential_participant auditorP{};
mpt_confidential_participant const* auditorPtr = nullptr;
if (auditor)
{
if (auditor->publicKey.size() != kEcPubKeyLength ||
auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
auditorP = toParticipant(*auditor);
auditorPtr = &auditorP;
}
if (mpt_verify_revealed_amount(amount, blindingFactor.data(), &holderP, &issuerP, auditorPtr) !=
0)
{
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() != kEcGamalEncryptedTotalLength ||
object[sfIssuerEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
{
return temBAD_CIPHERTEXT;
}
bool const hasAuditor = object.isFieldPresent(sfAuditorEncryptedAmount);
if (hasAuditor && object[sfAuditorEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (!isValidCiphertext(object[sfHolderEncryptedAmount]) ||
!isValidCiphertext(object[sfIssuerEncryptedAmount]))
{
return temBAD_CIPHERTEXT;
}
if (hasAuditor && !isValidCiphertext(object[sfAuditorEncryptedAmount]))
return temBAD_CIPHERTEXT;
return tesSUCCESS;
}
TER
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash)
{
if (proofSlice.size() != kEcSchnorrProofLength || pubKeySlice.size() != kEcPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (mpt_verify_convert_proof(proofSlice.data(), pubKeySlice.data(), contextHash.data()) != 0)
return tecBAD_PROOF;
return tesSUCCESS;
}
TER
verifyClawbackProof(
uint64_t const amount,
Slice const& proof,
Slice const& pubKeySlice,
Slice const& ciphertext,
uint256 const& contextHash)
{
if (ciphertext.size() != kEcGamalEncryptedTotalLength ||
pubKeySlice.size() != kEcPubKeyLength || proof.size() != kEcClawbackProofLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (mpt_verify_clawback_proof(
proof.data(), amount, pubKeySlice.data(), ciphertext.data(), contextHash.data()) != 0)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifySendProof(
Slice const& proof,
ConfidentialRecipient const& sender,
ConfidentialRecipient const& destination,
ConfidentialRecipient const& issuer,
std::optional<ConfidentialRecipient> const& auditor,
Slice const& spendingBalance,
Slice const& amountCommitment,
Slice const& balanceCommitment,
uint256 const& contextHash)
{
auto const recipientCount = getConfidentialRecipientCount(auditor.has_value());
if (proof.size() != kEcSendProofLength || sender.publicKey.size() != kEcPubKeyLength ||
sender.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
destination.publicKey.size() != kEcPubKeyLength ||
destination.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
issuer.publicKey.size() != kEcPubKeyLength ||
issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
spendingBalance.size() != kEcGamalEncryptedTotalLength ||
amountCommitment.size() != kEcPedersenCommitmentLength ||
balanceCommitment.size() != kEcPedersenCommitmentLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
std::vector<mpt_confidential_participant> participants;
participants.reserve(recipientCount);
participants.push_back(toParticipant(sender));
participants.push_back(toParticipant(destination));
participants.push_back(toParticipant(issuer));
if (auditor)
{
if (auditor->publicKey.size() != kEcPubKeyLength ||
auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
participants.push_back(toParticipant(*auditor));
}
if (participants.size() != recipientCount)
return tecINTERNAL; // LCOV_EXCL_LINE
if (mpt_verify_send_proof(
proof.data(),
participants.data(),
recipientCount,
spendingBalance.data(),
amountCommitment.data(),
balanceCommitment.data(),
contextHash.data()) != 0)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyConvertBackProof(
Slice const& proof,
Slice const& pubKeySlice,
Slice const& spendingBalance,
Slice const& balanceCommitment,
uint64_t amount,
uint256 const& contextHash)
{
if (proof.size() != kEcConvertBackProofLength || pubKeySlice.size() != kEcPubKeyLength ||
spendingBalance.size() != kEcGamalEncryptedTotalLength ||
balanceCommitment.size() != kEcPedersenCommitmentLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (mpt_verify_convert_back_proof(
proof.data(),
pubKeySlice.data(),
spendingBalance.data(),
balanceCommitment.data(),
amount,
contextHash.data()) != 0)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
} // namespace xrpl
+2 -1
View File
@@ -5,6 +5,7 @@
#include <xrpl/basics/contract.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/detail/secp256k1.h>
#include <xrpl/protocol/digest.h>
@@ -211,7 +212,7 @@ publicKeyType(Slice const& slice)
if (slice[0] == 0xED)
return KeyType::Ed25519;
if (slice[0] == 0x02 || slice[0] == 0x03)
if (slice[0] == kEcCompressedPrefixEvenY || slice[0] == kEcCompressedPrefixOddY)
return KeyType::Secp256k1;
}
+2
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."),
@@ -199,6 +200,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."),
+9
View File
@@ -356,6 +356,15 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
return baseFee + (signerCount * baseFee);
}
XRPAmount
Transactor::calculateBaseFee(
ReadView const& view,
STTx const& tx,
std::uint32_t extraBaseFeeMultiplier)
{
return calculateBaseFee(view, tx) + view.fees().base * extraBaseFeeMultiplier;
}
// Returns the fee in fee units, not scaled for load.
XRPAmount
Transactor::calculateOwnerReserveFee(ReadView const& view, STTx const& tx)
+302
View File
@@ -1,7 +1,9 @@
#include <xrpl/tx/invariants/MPTInvariant.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -21,11 +23,46 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
namespace xrpl {
namespace {
constexpr auto kConfidentialMptTxTypes = std::to_array<TxType>({
ttCONFIDENTIAL_MPT_SEND,
ttCONFIDENTIAL_MPT_CONVERT,
ttCONFIDENTIAL_MPT_CONVERT_BACK,
ttCONFIDENTIAL_MPT_MERGE_INBOX,
ttCONFIDENTIAL_MPT_CLAWBACK,
});
// Clamp to the cap (== INT64_MAX) before the signed conversion. Invariant
// tests can inject INT64_MAX + 1, which would result in undefined behavior
// under UBSan if converted directly.
std::int64_t
toSignedMPTAmount(std::uint64_t amount)
{
return static_cast<std::int64_t>(std::min(amount, kMaxMpTokenAmount));
}
std::int64_t
addMPTAmountDelta(std::int64_t delta, std::uint64_t amount)
{
return delta + toSignedMPTAmount(amount);
}
std::int64_t
subtractMPTAmountDelta(std::int64_t delta, std::uint64_t amount)
{
return delta - toSignedMPTAmount(amount);
}
} // namespace
void
ValidMPTIssuance::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
@@ -436,6 +473,16 @@ ValidMPTPayment::finalize(
{
if (isTesSuccess(result))
{
// Confidential transactions are validated by ValidConfidentialMPToken.
// They modify encrypted fields and sfConfidentialOutstandingAmount
// rather than sfMPTAmount/sfOutstandingAmount in the standard way,
// so ValidMPTPayment's accounting does not apply to them.
if (std::ranges::find(kConfidentialMptTxTypes, tx.getTxnType()) !=
kConfidentialMptTxTypes.end())
{
return true;
}
bool const invariantPasses = !view.rules().enabled(featureMPTokensV2);
if (overflow_)
{
@@ -466,6 +513,261 @@ ValidMPTPayment::finalize(
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::kZero;
if (sle->getType() == ltMPTOKEN)
return sle->getFieldH192(sfMPTokenIssuanceID);
if (sle->getType() == ltMPTOKEN_ISSUANCE)
return makeMptID(sle->getFieldU32(sfSequence), sle->getAccountID(sfIssuer));
return beast::kZero;
};
if (before && before->getType() == ltMPTOKEN)
{
uint192 const id = getMptID(before);
auto& change = changes_[id];
change.mptAmountDelta =
subtractMPTAmountDelta(change.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) ||
before->isFieldPresent(sfAuditorEncryptedBalance);
if (hasPublicBalance || hasEncryptedFields)
changes_[id].deletedWithEncrypted = true;
}
}
if (after && after->getType() == ltMPTOKEN)
{
uint192 const id = getMptID(after);
auto& change = changes_[id];
change.mptAmountDelta =
addMPTAmountDelta(change.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 hasAuditorBalance = after->isFieldPresent(sfAuditorEncryptedBalance);
// The core encrypted balances must all exist or not exist at the same time. The auditor
// balance is optional, but cannot exist without the core fields.
if (hasHolderInbox != hasHolderSpending || hasHolderInbox != hasIssuerBalance ||
(hasAuditorBalance && !hasIssuerBalance))
changes_[id].badConsistency = true;
auto const confidentialBalanceFieldChanged = [&before, &after](auto const& field) {
auto const afterValue = (*after)[~field];
if (!afterValue)
return false;
if (!before || before->getType() != ltMPTOKEN)
return true; // LCOV_EXCL_LINE
return (*before)[~field] != afterValue;
};
if (confidentialBalanceFieldChanged(sfConfidentialBalanceInbox) ||
confidentialBalanceFieldChanged(sfConfidentialBalanceSpending) ||
confidentialBalanceFieldChanged(sfIssuerEncryptedBalance) ||
confidentialBalanceFieldChanged(sfAuditorEncryptedBalance))
{
changes_[id].changesConfidentialFields = true;
}
}
if (before && before->getType() == ltMPTOKEN_ISSUANCE)
{
uint192 const id = getMptID(before);
auto& change = changes_[id];
if (before->isFieldPresent(sfConfidentialOutstandingAmount))
{
change.coaDelta = subtractMPTAmountDelta(
change.coaDelta, before->getFieldU64(sfConfidentialOutstandingAmount));
}
change.outstandingDelta = subtractMPTAmountDelta(
change.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 = addMPTAmountDelta(change.coaDelta, coa);
change.outstandingDelta = addMPTAmountDelta(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::mptokenIssuance(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;
}
// Confidential balance fields may remain on a holder MPToken after all
// confidential balances have returned to zero. Only creating or
// changing those fields requires the issuance privacy flag.
if (checks.changesConfidentialFields)
{
if (!issuance->isFlag(lsfMPTCanHoldConfidentialBalance))
{
JLOG(j.fatal()) << "Invariant failed: MPToken has encrypted "
"fields but Issuance does not have "
"lsfMPTCanHoldConfidentialBalance 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;
}
}
else if (
std::ranges::find(kConfidentialMptTxTypes, tx.getTxnType()) !=
kConfidentialMptTxTypes.end())
{
// Confidential Txns should not modify public MPTAmount balance
// if Confidential Amount Delta is 0
if (checks.mptAmountDelta != 0)
{
JLOG(j.fatal()) << "Invariant failed: MPTAmount changed by confidential "
"transaction that should not modify this field."
<< to_string(id);
return false;
}
// Among confidential MPT transactions, only ConfidentialMPTSend and
// ConfidentialMPTMergeInbox leave coaDelta unmodified. Therefore, if a confidential MPT
// transaction reaches here, it must be one of these two types, neither of which will
// modify sfOutstandingAmount
if (checks.outstandingDelta != 0)
{
JLOG(j.fatal()) << "Invariant failed: OutstandingAmount changed "
"by confidential transaction that should not "
"modify it 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;
}
void
ValidMPTTransfer::visitEntry(
bool isDelete,
@@ -0,0 +1,210 @@
#include <xrpl/tx/transactors/token/ConfidentialMPTClawback.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <memory>
#include <utility>
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 > kMaxMpTokenAmount)
return temBAD_AMOUNT;
// Verify proof length
if (ctx.tx[sfZKProof].length() != kEcClawbackProofLength)
return temMALFORMED;
return tesSUCCESS;
}
XRPAmount
ConfidentialMPTClawback::calculateBaseFee(ReadView const& view, STTx const& tx)
{
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
}
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::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// Sanity check: account must be the same as issuer
if (sleIssuance->getAccountID(sfIssuer) != account)
return tefINTERNAL; // LCOV_EXCL_LINE
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
return tecNO_PERMISSION;
// Check if clawback is allowed
if (!sleIssuance->isFlag(lsfMPTCanClawback))
return tecNO_PERMISSION;
// Check if issuance allows confidential transfer
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
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;
// Check if Holder has ElGamal public Key
if (!sleHolderMPToken->isFieldPresent(sfHolderEncryptionKey))
return tecNO_PERMISSION;
// Sanity check: claw amount can not exceed confidential outstanding amount
// or total outstanding amount (prevents underflow in doApply)
auto const amount = ctx.tx[sfMPTAmount];
if (amount > (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) ||
amount > (*sleIssuance)[sfOutstandingAmount])
return tecINSUFFICIENT_FUNDS;
auto const contextHash =
getClawbackContextHash(account, mptIssuanceID, ctx.tx.getSeqProxy().value(), holder);
// Verify the revealed confidential amount by the issuer matches the exact
// confidential balance of the holder.
return verifyClawbackProof(
amount,
ctx.tx[sfZKProof],
(*sleIssuance)[sfIssuerEncryptionKey],
(*sleHolderMPToken)[sfIssuerEncryptedBalance],
contextHash);
}
TER
ConfidentialMPTClawback::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto const holder = ctx_.tx[sfHolder];
auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder));
if (!sleIssuance || !sleHolderMPToken)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const clawAmount = ctx_.tx[sfMPTAmount];
auto const holderPubKey = (*sleHolderMPToken)[sfHolderEncryptionKey];
auto const issuerPubKey = (*sleIssuance)[sfIssuerEncryptionKey];
// After clawback, the balance should be encrypted zero.
auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
if (!encZeroForHolder)
return tecINTERNAL; // LCOV_EXCL_LINE
auto 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] = std::move(*encZeroForIssuer);
incrementConfidentialVersion(*sleHolderMPToken);
if (sleHolderMPToken->isFieldPresent(sfAuditorEncryptedBalance))
{
// Sanity check: the issuance must have an auditor public key if
// auditing is enabled.
if (!sleIssuance->isFieldPresent(sfAuditorEncryptionKey))
return tecINTERNAL; // LCOV_EXCL_LINE
auto const auditorPubKey = (*sleIssuance)[sfAuditorEncryptionKey];
auto encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID);
if (!encZeroForAuditor)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor);
}
// Decrease Global Confidential Outstanding Amount
auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount];
if (clawAmount > oldCOA)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleIssuance)[sfConfidentialOutstandingAmount] = oldCOA - clawAmount;
// Decrease Global Total Outstanding Amount
auto const oldOA = (*sleIssuance)[sfOutstandingAmount];
if (clawAmount > oldOA)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleIssuance)[sfOutstandingAmount] = oldOA - clawAmount;
view().update(sleHolderMPToken);
view().update(sleIssuance);
return tesSUCCESS;
}
void
ConfidentialMPTClawback::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
}
bool
ConfidentialMPTClawback::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl
@@ -0,0 +1,350 @@
#include <xrpl/tx/transactors/token/ConfidentialMPTConvert.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <memory>
#include <optional>
#include <utility>
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] > kMaxMpTokenAmount)
return temBAD_AMOUNT;
if (ctx.tx.isFieldPresent(sfHolderEncryptionKey))
{
if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
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 registering holder ec public key
if (ctx.tx[sfZKProof].size() != kEcSchnorrProofLength)
return temMALFORMED;
}
else
{
// Either both sfHolderEncryptionKey and sfZKProof should be present, or both should be
// absent.
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;
}
XRPAmount
ConfidentialMPTConvert::calculateBaseFee(ReadView const& view, STTx const& tx)
{
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
}
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::mptokenIssuance(issuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) ||
!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
{
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
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
// 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};
// Explicit freeze and auth checks are required because accountHolds
// with ZeroIfFrozen/ZeroIfUnauthorized only implicitly rejects
// non-zero amounts. A zero-amount convert would bypass those implicit
// checks, allowing frozen or unauthorized accounts to register ElGamal
// keys and initialize confidential balance fields.
// Check lock
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;
auto const mptAmount =
STAmount(MPTAmount{static_cast<MPTAmount::value_type>(amount)}, mptIssue);
if (accountHolds(
ctx.view,
account,
mptIssue,
FreezeHandling::ZeroIfFrozen,
AuthHandling::ZeroIfUnauthorized,
ctx.j) < mptAmount)
{
return tecINSUFFICIENT_FUNDS;
}
auto const hasHolderKeyOnLedger = sleMptoken->isFieldPresent(sfHolderEncryptionKey);
auto const hasHolderKeyInTx = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
// 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;
// Run all verifications before returning any error to prevent timing attacks
// that could reveal which proof failed.
bool valid = true;
Slice holderPubKey;
if (hasHolderKeyInTx)
{
holderPubKey = ctx.tx[sfHolderEncryptionKey];
auto const contextHash =
getConvertContextHash(account, issuanceID, ctx.tx.getSeqProxy().value());
if (auto const ter = verifySchnorrProof(holderPubKey, ctx.tx[sfZKProof], contextHash);
!isTesSuccess(ter))
{
valid = false;
}
}
else
{
holderPubKey = (*sleMptoken)[sfHolderEncryptionKey];
}
std::optional<ConfidentialRecipient> auditor;
if (hasAuditor)
{
auditor.emplace(
ConfidentialRecipient{
.publicKey = (*sleIssuance)[sfAuditorEncryptionKey],
.encryptedAmount = ctx.tx[sfAuditorEncryptedAmount],
});
}
auto const blindingFactor = ctx.tx[sfBlindingFactor];
if (auto const ter = verifyRevealedAmount(
amount,
Slice(blindingFactor.data(), blindingFactor.size()),
{
.publicKey = holderPubKey,
.encryptedAmount = ctx.tx[sfHolderEncryptedAmount],
},
{
.publicKey = (*sleIssuance)[sfIssuerEncryptionKey],
.encryptedAmount = ctx.tx[sfIssuerEncryptedAmount],
},
auditor);
!isTesSuccess(ter))
{
valid = false;
}
if (!valid)
return tecBAD_PROOF;
return tesSUCCESS;
}
TER
ConfidentialMPTConvert::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const amtToConvert = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);
if (ctx_.tx.isFieldPresent(sfHolderEncryptionKey))
(*sleMptoken)[sfHolderEncryptionKey] = ctx_.tx[sfHolderEncryptionKey];
// Converting decreases regular balance and increases confidential outstanding.
// The confidential outstanding tracks total tokens in confidential form globally.
auto const currentCOA = (*sleIssuance)[~sfConfidentialOutstandingAmount].valueOr(0);
if (amtToConvert > kMaxMpTokenAmount - currentCOA)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfMPTAmount] = amt - amtToConvert;
(*sleIssuance)[sfConfidentialOutstandingAmount] = currentCOA + amtToConvert;
auto const holderEc = ctx_.tx[sfHolderEncryptedAmount];
auto 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)
{
auto sum = homomorphicAdd(holderEc, (*sleMptoken)[sfConfidentialBalanceInbox]);
if (!sum)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTConvert failed homomorphic add for holder inbox.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*sum);
}
// homomorphically add issuer's encrypted balance
{
auto sum = homomorphicAdd(issuerEc, (*sleMptoken)[sfIssuerEncryptedBalance]);
if (!sum)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTConvert failed homomorphic add for issuer balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*sum);
}
// homomorphically add auditor's encrypted balance
if (auditorEc)
{
if (!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
return tecINTERNAL; // LCOV_EXCL_LINE
auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]);
if (!sum)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTConvert failed homomorphic add for auditor balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*sum);
}
}
else if (
!sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) &&
!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
{
// 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 zeroBalance = encryptCanonicalZeroAmount(
(*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
if (!zeroBalance)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance);
}
else
{
// both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
// exist together
return tecINTERNAL; // LCOV_EXCL_LINE
}
view().update(sleIssuance);
view().update(sleMptoken);
return tesSUCCESS;
}
void
ConfidentialMPTConvert::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
}
bool
ConfidentialMPTConvert::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl
@@ -0,0 +1,319 @@
#include <xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <memory>
#include <optional>
#include <utility>
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] > kMaxMpTokenAmount)
return temBAD_AMOUNT;
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 = compact sigma proof (128 bytes) + single bulletproof (688 bytes)
if (ctx.tx[sfZKProof].size() != kEcConvertBackProofLength)
return temMALFORMED;
return tesSUCCESS;
}
XRPAmount
ConfidentialMPTConvertBack::calculateBaseFee(ReadView const& view, STTx const& tx)
{
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
}
/**
* 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(sfHolderEncryptionKey))
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)[sfHolderEncryptionKey];
auto const contextHash = getConvertBackContextHash(
account,
mptIssuanceID,
tx.getSeqProxy().value(),
(*mptoken)[~sfConfidentialBalanceVersion].value_or(0));
// Prepare Auditor Info
std::optional<ConfidentialRecipient> auditor;
bool const hasAuditor = issuance->isFieldPresent(sfAuditorEncryptionKey);
if (hasAuditor)
{
auditor.emplace(
ConfidentialRecipient{
.publicKey = (*issuance)[sfAuditorEncryptionKey],
.encryptedAmount = tx[sfAuditorEncryptedAmount],
});
}
// Run all verifications before returning any error to prevent timing attacks
// that could reveal which proof failed.
bool valid = true;
if (auto const ter = verifyRevealedAmount(
amount,
Slice(blindingFactor.data(), blindingFactor.size()),
{
.publicKey = holderPubKey,
.encryptedAmount = tx[sfHolderEncryptedAmount],
},
{
.publicKey = (*issuance)[sfIssuerEncryptionKey],
.encryptedAmount = tx[sfIssuerEncryptedAmount],
},
auditor);
!isTesSuccess(ter))
{
valid = false;
}
if (auto const ter = verifyConvertBackProof(
tx[sfZKProof],
holderPubKey,
(*mptoken)[sfConfidentialBalanceSpending],
tx[sfBalanceCommitment],
amount,
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::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) ||
!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
return tecNO_PERMISSION;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
// 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(sfHolderEncryptionKey) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfIssuerEncryptedBalance))
{
return tecNO_PERMISSION;
}
// Sanity check: holder's MPToken must have auditor balance field if auditing
// is enabled
if (requiresAuditor && !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
return tefINTERNAL; // LCOV_EXCL_LINE
// 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 (auto 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, accountID_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const amtToConvertBack = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);
// Converting back increases regular balance and decreases confidential
// outstanding. This is the inverse of Convert.
if (amt > kMaxMpTokenAmount - amtToConvertBack)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack;
auto const coa = (*sleIssuance)[~sfConfidentialOutstandingAmount].valueOr(0);
if (coa < amtToConvertBack)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleIssuance)[sfConfidentialOutstandingAmount] = coa - amtToConvertBack;
std::optional<Slice> const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
// homomorphically subtract holder's encrypted balance
{
auto res = homomorphicSubtract(
(*sleMptoken)[sfConfidentialBalanceSpending], ctx_.tx[sfHolderEncryptedAmount]);
if (!res)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTConvertBack failed homomorphic subtract for holder spending "
"balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*res);
}
// homomorphically subtract issuer's encrypted balance
{
auto res = homomorphicSubtract(
(*sleMptoken)[sfIssuerEncryptedBalance], ctx_.tx[sfIssuerEncryptedAmount]);
if (!res)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTConvertBack failed homomorphic subtract for issuer balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*res);
}
if (auditorEc)
{
auto res = homomorphicSubtract(
(*sleMptoken)[sfAuditorEncryptedBalance], ctx_.tx[sfAuditorEncryptedAmount]);
if (!res)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTConvertBack failed homomorphic subtract for auditor balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*res);
}
incrementConfidentialVersion(*sleMptoken);
view().update(sleIssuance);
view().update(sleMptoken);
return tesSUCCESS;
}
void
ConfidentialMPTConvertBack::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
}
bool
ConfidentialMPTConvertBack::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl
@@ -0,0 +1,150 @@
#include <xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <memory>
#include <utility>
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;
}
XRPAmount
ConfidentialMPTMergeInbox::calculateBaseFee(ReadView const& view, STTx const& tx)
{
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
}
TER
ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx)
{
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
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(sfHolderEncryptionKey))
{
return tecNO_PERMISSION;
}
// Check lock
auto const account = ctx.tx[sfAccount];
MPTIssue const mptIssue(ctx.tx[sfMPTokenIssuanceID]);
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;
return tesSUCCESS;
}
TER
ConfidentialMPTMergeInbox::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
// sanity check
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleMptoken->isFieldPresent(sfHolderEncryptionKey))
{
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.
auto sum = homomorphicAdd(
(*sleMptoken)[sfConfidentialBalanceSpending], (*sleMptoken)[sfConfidentialBalanceInbox]);
if (!sum)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTMergeInbox failed homomorphic add for inbox merge.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*sum);
// Reset inbox to encrypted zero. Must use canonical zero encryption
// (deterministic ciphertext) so the ledger state is reproducible.
auto zeroEncryption =
encryptCanonicalZeroAmount((*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
if (!zeroEncryption)
return tecINTERNAL; // LCOV_EXCL_LINE
(*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*zeroEncryption);
incrementConfidentialVersion(*sleMptoken);
view().update(sleMptoken);
return tesSUCCESS;
}
void
ConfidentialMPTMergeInbox::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
}
bool
ConfidentialMPTMergeInbox::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl
@@ -0,0 +1,445 @@
#include <xrpl/tx/transactors/token/ConfidentialMPTSend.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <memory>
#include <optional>
#include <utility>
namespace xrpl {
bool
ConfidentialMPTSend::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
}
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;
// Issuer cannot be the destination
if (ctx.tx[sfDestination] == issuer)
return temMALFORMED;
// Check the length of the encrypted amounts
if (ctx.tx[sfSenderEncryptedAmount].length() != kEcGamalEncryptedTotalLength ||
ctx.tx[sfDestinationEncryptedAmount].length() != kEcGamalEncryptedTotalLength ||
ctx.tx[sfIssuerEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
{
return temBAD_CIPHERTEXT;
}
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
if (hasAuditor && ctx.tx[sfAuditorEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
// Check the length of the ZKProof (fixed size regardless of recipient count)
if (ctx.tx[sfZKProof].length() != kEcSendProofLength)
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;
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}
XRPAmount
ConfidentialMPTSend::calculateBaseFee(ReadView const& view, STTx const& tx)
{
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
}
namespace detail {
static 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);
std::optional<ConfidentialRecipient> auditor;
if (hasAuditor)
{
auditor.emplace(
ConfidentialRecipient{
.publicKey = (*sleIssuance)[sfAuditorEncryptionKey],
.encryptedAmount = ctx.tx[sfAuditorEncryptedAmount],
});
}
auto const contextHash = getSendContextHash(
ctx.tx[sfAccount],
ctx.tx[sfMPTokenIssuanceID],
ctx.tx.getSeqProxy().value(),
ctx.tx[sfDestination],
(*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0));
return verifySendProof(
ctx.tx[sfZKProof],
{
.publicKey = (*sleSenderMPToken)[sfHolderEncryptionKey],
.encryptedAmount = ctx.tx[sfSenderEncryptedAmount],
},
{
.publicKey = (*sleDestinationMPToken)[sfHolderEncryptionKey],
.encryptedAmount = ctx.tx[sfDestinationEncryptedAmount],
},
{
.publicKey = (*sleIssuance)[sfIssuerEncryptionKey],
.encryptedAmount = ctx.tx[sfIssuerEncryptedAmount],
},
auditor,
(*sleSenderMPToken)[sfConfidentialBalanceSpending],
ctx.tx[sfAmountCommitment],
ctx.tx[sfBalanceCommitment],
contextHash);
}
} // namespace detail
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];
auto const sleDst = ctx.view.read(keylet::account(destination));
if (!sleDst)
return tecNO_TARGET;
// Check destination tag
if (((sleDst->getFlags() & lsfRequireDestTag) != 0u) &&
!ctx.tx.isFieldPresent(sfDestinationTag))
{
return tecDST_TAG_NEEDED;
}
// Check if MPT issuance exists
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(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(lsfMPTCanHoldConfidentialBalance))
return tecNO_PERMISSION;
// Sanity check: transfer fee must be 0 for confidential MPTs. This should
// be unreachable in valid ledger state because MPTokenIssuanceCreate and
// MPTokenIssuanceSet enforce it.
if ((*sleIssuance)[~sfTransferFee].value_or(0) > 0)
return tecNO_PERMISSION;
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
return tecNO_PERMISSION;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
// 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(sfHolderEncryptionKey) ||
!sleSenderMPToken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleSenderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
{
return tecNO_PERMISSION;
}
// 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(sfHolderEncryptionKey) ||
!sleDestinationMPToken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleDestinationMPToken->isFieldPresent(sfIssuerEncryptedBalance))
{
return tecNO_PERMISSION;
}
// Sanity check: Both MPTokens' auditor fields must be present if auditing
// is enabled
if (requiresAuditor &&
(!sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance) ||
!sleDestinationMPToken->isFieldPresent(sfAuditorEncryptedBalance)))
{
return tefINTERNAL; // LCOV_EXCL_LINE
}
// 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;
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
// Check deposit preauth before the expensive ZK proof verification.
// Uses read-only view.
auto const preauthErr =
checkDepositPreauth(ctx.tx, ctx.view, account, destination, sleDst, ctx.j);
if (!isTesSuccess(preauthErr))
return preauthErr;
return detail::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, accountID_));
auto sleDestinationMPToken = view().peek(keylet::mptoken(mptIssuanceID, destination));
auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID));
auto const sleDestAcct = view().read(keylet::account(destination));
if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance || !sleDestAcct)
return tecINTERNAL; // LCOV_EXCL_LINE
// Deposit preauth authorization was already verified in preclaim.
// Remove any expired credentials.
if (auto err = cleanupExpiredCredentials(ctx_.tx, ctx_.view(), ctx_.journal);
!isTesSuccess(err))
return err;
auto const senderEc = ctx_.tx[sfSenderEncryptedAmount];
auto const destEc = ctx_.tx[sfDestinationEncryptedAmount];
auto const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
auto const proof = ctx_.tx[sfZKProof];
Slice const sendChallenge{proof.data(), kEcBlindingFactorLength};
auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
// Subtract from sender's spending balance
{
auto const curSpending = (*sleSenderMPToken)[sfConfidentialBalanceSpending];
auto newSpending = homomorphicSubtract(curSpending, senderEc);
if (!newSpending)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTSend failed homomorphic subtract for sender spending balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleSenderMPToken)[sfConfidentialBalanceSpending] = std::move(*newSpending);
}
// Subtract from issuer's balance
{
auto const curIssuerEnc = (*sleSenderMPToken)[sfIssuerEncryptedBalance];
auto newIssuerEnc = homomorphicSubtract(curIssuerEnc, issuerEc);
if (!newIssuerEnc)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTSend failed homomorphic subtract for sender issuer balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleSenderMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc);
}
// Subtract from auditor's balance if present
if (auditorEc)
{
auto const curAuditorEnc = (*sleSenderMPToken)[sfAuditorEncryptedBalance];
auto newAuditorEnc = homomorphicSubtract(curAuditorEnc, *auditorEc);
if (!newAuditorEnc)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTSend failed homomorphic subtract for sender auditor balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleSenderMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc);
}
// Add to destination's inbox balance
{
auto rerandomizedDestEc = rerandomizeCiphertext(
destEc, (*sleDestinationMPToken)[sfHolderEncryptionKey], sendChallenge);
if (!rerandomizedDestEc)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox];
auto newInbox = homomorphicAdd(curInbox, *rerandomizedDestEc);
if (!newInbox)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTSend failed homomorphic add for destination inbox.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleDestinationMPToken)[sfConfidentialBalanceInbox] = std::move(*newInbox);
}
// Add to issuer's balance
{
auto rerandomizedIssuerEc =
rerandomizeCiphertext(issuerEc, (*sleIssuance)[sfIssuerEncryptionKey], sendChallenge);
if (!rerandomizedIssuerEc)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance];
auto newIssuerEnc = homomorphicAdd(curIssuerEnc, *rerandomizedIssuerEc);
if (!newIssuerEnc)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTSend failed homomorphic add for destination issuer balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleDestinationMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc);
}
// Add to auditor's balance if present
if (auditorEc)
{
auto rerandomizedAuditorEc = rerandomizeCiphertext(
*auditorEc, (*sleIssuance)[sfAuditorEncryptionKey], sendChallenge);
if (!rerandomizedAuditorEc)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance];
auto newAuditorEnc = homomorphicAdd(curAuditorEnc, *rerandomizedAuditorEc);
if (!newAuditorEnc)
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTSend failed homomorphic add for destination auditor balance.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleDestinationMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc);
}
// increment sender version only; receiver version is not modified by incoming sends
incrementConfidentialVersion(*sleSenderMPToken);
view().update(sleSenderMPToken);
view().update(sleDestinationMPToken);
return tesSUCCESS;
}
void
ConfidentialMPTSend::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
}
bool
ConfidentialMPTSend::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl
@@ -83,6 +83,25 @@ 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::mptokenIssuance(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;
}
@@ -37,7 +37,15 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
if (ctx.tx.isFieldPresent(sfMutableFlags) && !ctx.rules.enabled(featureDynamicMPT))
return false;
return true;
if (ctx.tx.isFlag(tfMPTCanHoldConfidentialBalance) &&
!ctx.rules.enabled(featureConfidentialTransfer))
return false;
// can not set tmfMPTCannotEnableCanHoldConfidentialBalance without featureConfidentialTransfer
auto const mutableFlags = ctx.tx[~sfMutableFlags];
return !mutableFlags ||
((*mutableFlags & tmfMPTCannotEnableCanHoldConfidentialBalance) == 0u) ||
ctx.rules.enabled(featureConfidentialTransfer);
}
std::uint32_t
@@ -70,6 +78,10 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
// must also be set.
if (fee > 0u && !ctx.tx.isFlag(tfMPTCanTransfer))
return temMALFORMED;
// Confidential amounts are encrypted so transfer rate is disallowed.
if (fee > 0u && ctx.tx.isFlag(tfMPTCanHoldConfidentialBalance))
return temBAD_TRANSFER_FEE;
}
if (auto const domain = ctx.tx[~sfDomainID])
@@ -5,6 +5,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
@@ -74,11 +75,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(sfIssuerEncryptionKey);
auto const hasAuditorElGamalKey = ctx.tx.isFieldPresent(sfAuditorEncryptionKey);
auto const txFlags = ctx.tx.getFlags();
bool const enablePrivacy =
mutableFlags && (*mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u;
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 || enablePrivacy) &&
!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
if (hasDomain && hasHolder)
return temMALFORMED;
if (enablePrivacy && hasHolder)
return temMALFORMED;
// fails if both flags are set
@@ -90,10 +107,12 @@ 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 (ctx.tx.getFlags() == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate)
if (txFlags == 0 && !hasDomain && !hasIssuerElGamalKey && !hasAuditorElGamalKey &&
!isMutate)
return temMALFORMED;
}
@@ -110,6 +129,9 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (transferFee && *transferFee > kMaxTransferFee)
return temBAD_TRANSFER_FEE;
if (transferFee && *transferFee > 0u && enablePrivacy)
return temBAD_TRANSFER_FEE;
if (metadata && metadata->length() > kMaxMpTokenMetadataLength)
return temMALFORMED;
@@ -120,6 +142,18 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
}
}
if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
return temMALFORMED;
if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
return temMALFORMED;
if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey]))
return temMALFORMED;
if (hasAuditorElGamalKey && !isValidCompressedECPoint(ctx.tx[sfAuditorEncryptionKey]))
return temMALFORMED;
return tesSUCCESS;
}
@@ -181,12 +215,20 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return currentMutableFlags & mutableFlag;
};
if (auto const mutableFlags = ctx.tx[~sfMutableFlags])
auto const mutableFlags = ctx.tx[~sfMutableFlags];
// Whether the transaction is enabling confidential amounts.
bool const enablesConfidentialAmount =
mutableFlags && (*mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u;
if (mutableFlags)
{
if (std::ranges::any_of(kMptMutabilityFlags, [mutableFlags, &isMutableFlag](auto const& f) {
return !isMutableFlag(f.canEnableFlag) && ((*mutableFlags & f.setFlag) != 0u);
}))
return tecNO_PERMISSION;
if (enablesConfidentialAmount &&
isMutableFlag(lsmfMPTCannotEnableCanHoldConfidentialBalance))
return tecNO_PERMISSION;
}
if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata))
@@ -201,10 +243,55 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
if (fee > 0u && !sleMptIssuance->isFlag(lsfMPTCanTransfer))
return tecNO_PERMISSION;
// Cannot set a non-zero TransferFee on an issuance that has confidential
// transfer enabled
if (fee > 0u && sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
return tecNO_PERMISSION;
if (!isMutableFlag(lsmfMPTCanMutateTransferFee))
return tecNO_PERMISSION;
}
// cannot update issuer public key
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey))
{
return tecNO_PERMISSION;
}
// cannot update auditor public key
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey))
{
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}
if (enablesConfidentialAmount && sleMptIssuance->isFieldPresent(sfTransferFee) &&
(*sleMptIssuance)[sfTransferFee] > 0u)
return tecNO_PERMISSION;
// Encryption keys can only be set if confidential amounts are already
// enabled on the issuance OR if the transaction is enabling it
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount)
{
return tecNO_PERMISSION;
}
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount)
{
return tecNO_PERMISSION;
}
// cannot upload key if there's circulating supply of COA
if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) ||
ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialAmount) &&
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
{
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
@@ -249,6 +336,9 @@ MPTokenIssuanceSet::doApply()
flagsOut |= f.ledgerFlag;
}
}
if ((mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u)
flagsOut |= lsfMPTCanHoldConfidentialBalance;
}
if (flagsIn != flagsOut)
@@ -300,6 +390,26 @@ MPTokenIssuanceSet::doApply()
}
}
if (auto const pubKey = ctx_.tx[~sfIssuerEncryptionKey])
{
// This is enforced in preflight.
XRPL_ASSERT(
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
sle->setFieldVL(sfIssuerEncryptionKey, *pubKey);
}
if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey])
{
// This is enforced in preflight.
XRPL_ASSERT(
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
sle->setFieldVL(sfAuditorEncryptionKey, *pubKey);
}
view().update(sle);
return tesSUCCESS;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -2504,7 +2504,9 @@ class Delegate_test : public beast::unit_test::Suite
// DO NOT modify expectedDelegableCount unless all scenarios, including
// edge cases, have been fully tested and verified.
// ====================================================================
std::size_t const expectedDelegableCount = 51;
// Includes the five confidential MPT transaction types, which are
// explicitly marked Delegable in transactions.macro.
std::size_t const expectedDelegableCount = 56;
BEAST_EXPECTS(
delegableCount == expectedDelegableCount,
+249
View File
@@ -4971,6 +4971,254 @@ class Invariants_test : public beast::unit_test::Suite
}
}
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 | tfMPTCanHoldConfidentialBalance});
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);
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;
sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
sleToken->makeFieldAbsent(sfConfidentialBalanceInbox);
sleToken->makeFieldAbsent(sfConfidentialBalanceSpending);
sleToken->setFieldVL(sfAuditorEncryptedBalance, Blob{0x00});
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 tfMPTCanHoldConfidentialBalance 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 "
"lsfMPTCanHoldConfidentialBalance "
"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 all three encrypted fields consistently (inbox+spending+issuer must be
// in sync or badConsistency fires first and masks requiresPrivacyFlag).
sleToken->setFieldVL(sfConfidentialBalanceInbox, Blob{0x00});
sleToken->setFieldVL(sfConfidentialBalanceSpending, 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::mptokenIssuance(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::mptokenIssuance(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);
// Send/MergeInbox must not change OutstandingAmount (coaDelta == 0)
doInvariantCheck(
{"Invariant failed: OutstandingAmount changed "
"by confidential transaction that should not "
"modify it for MPT"},
[&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
if (!sleIssuance)
return false;
sleIssuance->setFieldU64(
sfOutstandingAmount, sleIssuance->getFieldU64(sfOutstandingAmount) + 1);
ac.view().update(sleIssuance);
return true;
},
XRPAmount{},
STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseConfidential);
// Send/MergeInbox and zero-COA-delta confidential transactions must not
// change public holder MPTAmount.
doInvariantCheck(
{"Invariant failed: MPTAmount changed by confidential "
"transaction that should not modify this field."},
[&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
if (!sleToken)
return false;
sleToken->setFieldU64(sfMPTAmount, sleToken->getFieldU64(sfMPTAmount) + 1);
ac.view().update(sleToken);
return true;
},
XRPAmount{},
STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseConfidential);
// badVersion
doInvariantCheck(
{"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending "
"changed"},
[&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
Blob const kChangedConfidentialSpending = {0xBA, 0xDD};
auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
if (!sleToken)
return false;
sleToken->setFieldVL(sfConfidentialBalanceSpending, kChangedConfidentialSpending);
// 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 | tfMPTCanHoldConfidentialBalance});
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
@@ -4999,6 +5247,7 @@ public:
testValidPseudoAccounts();
testValidLoanBroker();
testVault();
testConfidentialMPTTransfer();
testMPT();
testInvariantOverwrite(defaultAmendments());
testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3);
+2 -1
View File
@@ -617,7 +617,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});
+42
View File
@@ -39,6 +39,7 @@
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
@@ -6256,6 +6257,46 @@ class Vault_test : public beast::unit_test::Suite
runTest(amendments);
}
void
testRemoveEmptyHoldingConfidentialBalances()
{
testcase("removeEmptyHolding keeps MPToken with confidential balances");
using namespace test::jtx;
Env env{*this, testableAmendments()};
Account const issuer{"issuer"};
Account const holder{"holder"};
MPTTester mpt{env, issuer, {.holders = {holder}}};
mpt.create({.authorize = MPTCreate::allHolders});
auto const tokenKeylet = keylet::mptoken(mpt.issuanceID(), holder.id());
auto const encryptedBalanceFields = {
&sfConfidentialBalanceInbox,
&sfConfidentialBalanceSpending,
&sfIssuerEncryptedBalance,
&sfAuditorEncryptedBalance};
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
for (auto const field : encryptedBalanceFields)
{
Sandbox sb(&view, TapNone);
auto const token = sb.peek(tokenKeylet);
if (!BEAST_EXPECT(token))
return false;
token->setFieldVL(*field, gMakeZeroBuffer(kEcGamalEncryptedTotalLength));
sb.update(token);
BEAST_EXPECT(
removeEmptyHolding(sb, holder.id(), MPTIssue(mpt.issuanceID()), j) ==
tecHAS_OBLIGATIONS);
BEAST_EXPECT(sb.peek(tokenKeylet) != nullptr);
}
return true;
});
}
// -----------------------------------------------------------------------
// Helpers and tests: sole-shareholder / stuck-depositor (XLS-0065 +
// fixCleanup3_2_0). The vault-level withdraw behavior is tested here;
@@ -7854,6 +7895,7 @@ public:
testAssetsMaximum();
testBug6LimitBypassWithShares();
testRemoveEmptyHoldingLockedAmount();
testRemoveEmptyHoldingConfidentialBalances();
testWithdrawSoleShareholderFixedAssetExit(all_ - fixCleanup3_2_0);
testWithdrawSoleShareholderFixedAssetExit(all_);
+496
View File
@@ -0,0 +1,496 @@
#pragma once
#include <test/jtx/AMM.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/batch.h>
#include <test/jtx/credentials.h>
#include <test/jtx/delegate.h>
#include <test/jtx/deposit.h>
#include <test/jtx/flags.h>
#include <test/jtx/mpt.h>
#include <test/jtx/pay.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <utility/mpt_utility.h>
#include <secp256k1.h>
#include <secp256k1_mpt.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <functional>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
namespace xrpl {
class ConfidentialTransferTestBase : public beast::unit_test::Suite
{
protected:
template <class T>
static T
requireOptional(std::optional<T> value, char const* message)
{
if (!value)
Throw<std::runtime_error>(message);
return std::move(*value);
}
template <class T>
static T const&
requireOptionalRef(std::optional<T> const& value, char const* message)
{
if (!value)
Throw<std::runtime_error>(message);
return *value;
}
// Offset where the bulletproof begins in a send proof blob.
// Proof layout: [compact_sigma | bulletproof]
static constexpr size_t kBulletproofOffset = kEcSendProofLength - kEcDoubleBulletproofLength;
// Generate a forged aggregated bulletproof (double bulletproof) for
// the given values and blinding factors. Used to test that splicing
// a bulletproof claiming a different remaining balance is rejected.
// secp256k1 convention: returns 1 on success, 0 on failure.
static Buffer
getForgedBulletproof(
std::array<uint64_t, 2> const& values,
std::array<Buffer, 2> const& blindingFactors,
uint256 const& contextHash)
{
auto* const ctx = mpt_secp256k1_context();
secp256k1_pubkey h;
secp256k1_mpt_get_h_generator(ctx, &h);
Buffer proof(kEcDoubleBulletproofLength);
size_t proofLen = kEcDoubleBulletproofLength;
unsigned char blindings[64];
std::memcpy(blindings, blindingFactors[0].data(), 32);
std::memcpy(blindings + 32, blindingFactors[1].data(), 32);
if (secp256k1_bulletproof_prove_agg(
ctx,
proof.data(),
&proofLen,
values.data(),
blindings,
2,
&h,
contextHash.data()) == 0)
Throw<std::runtime_error>("Failed to generate forged bulletproof");
return proof;
}
// Get a bad ciphertext with valid structure but cryptographic invalid for
// testing purposes. For preflight test purposes.
static Buffer const&
getBadCiphertext()
{
static Buffer const kBadCiphertext = []() {
Buffer buf(kEcGamalEncryptedTotalLength);
std::memset(buf.data(), 0xFF, kEcGamalEncryptedTotalLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
return buf;
}();
return kBadCiphertext;
}
// Get a trivial buffer that is structurally and mathematically valid, but
// contains invalid data that does not match the ledger state. For preclaim
// test purposes.
static Buffer const&
getTrivialCiphertext()
{
static Buffer const kTrivialCiphertext = []() {
Buffer buf(kEcGamalEncryptedTotalLength);
std::memset(buf.data(), 0, kEcGamalEncryptedTotalLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
buf.data()[kEcCiphertextComponentLength - 1] = 0x01;
buf.data()[kEcGamalEncryptedTotalLength - 1] = 0x01;
return buf;
}();
return kTrivialCiphertext;
}
// Returns a valid compressed EC point (33 bytes) that can pass preflight
// validation but contains invalid data for preclaim test purposes.
static Buffer const&
getTrivialCommitment()
{
static Buffer const kTrivialCommitment = []() {
Buffer buf(kEcPedersenCommitmentLength);
std::memset(buf.data(), 0, kEcPedersenCommitmentLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
// Set last byte to make it a valid x-coordinate on the curve
buf.data()[kEcPedersenCommitmentLength - 1] = 0x01;
return buf;
}();
return kTrivialCommitment;
}
static std::string
getTrivialSendProofHex()
{
Buffer buf(kEcSendProofLength);
std::memset(buf.data(), 0, kEcSendProofLength);
for (std::size_t i = 0; i < kEcSendProofLength; i += kEcCiphertextComponentLength)
{
buf.data()[i] = kEcCompressedPrefixEvenY;
if (i + kEcCiphertextComponentLength - 1 < kEcSendProofLength)
buf.data()[i + kEcCiphertextComponentLength - 1] = 0x01;
}
return strHex(buf);
}
// Helper struct to encapsulate common setup for integration tests.
struct ConfidentialSendSetup
{
// Constants
uint64_t sendAmount;
size_t nRecipients;
uint32_t version;
// Blinding factors
Buffer blindingFactor;
Buffer amountBlindingFactor;
Buffer balanceBlindingFactor;
// Encrypted amounts
Buffer senderAmt;
Buffer destAmt;
Buffer issuerAmt;
std::optional<Buffer> auditorAmt;
// Commitments
Buffer amountCommitment;
// Long-lived pub key buffers (to avoid dangling Slice)
Buffer senderPubKey;
Buffer destPubKey;
Buffer issuerPubKey;
std::optional<Buffer> auditorPubKey;
// Balance data
uint64_t prevSpending;
Buffer prevEncryptedSpending;
// Balance commitment (declared after prevSpending for init order)
Buffer balanceCommitment;
// Recipients vector
std::vector<ConfidentialRecipient> recipients;
// Constructor that performs all common setup
ConfidentialSendSetup(
test::jtx::MPTTester& mpt,
test::jtx::Account const& sender,
test::jtx::Account const& dest,
test::jtx::Account const& issuer,
uint64_t amount,
std::optional<std::reference_wrapper<test::jtx::Account const>> auditor = std::nullopt)
: sendAmount(amount)
, nRecipients(auditor ? 4 : 3)
, version(mpt.getMPTokenVersion(sender))
, blindingFactor(generateBlindingFactor())
, amountBlindingFactor(blindingFactor)
, balanceBlindingFactor(generateBlindingFactor())
, senderAmt(mpt.encryptAmount(sender, amount, blindingFactor))
, destAmt(mpt.encryptAmount(dest, amount, blindingFactor))
, issuerAmt(mpt.encryptAmount(issuer, amount, blindingFactor))
, auditorAmt(
auditor ? std::optional<Buffer>(
mpt.encryptAmount(auditor->get(), amount, blindingFactor))
: std::nullopt)
, amountCommitment(mpt.getPedersenCommitment(amount, amountBlindingFactor))
, senderPubKey(requireOptional(mpt.getPubKey(sender), "Missing sender public key"))
, destPubKey(requireOptional(mpt.getPubKey(dest), "Missing destination public key"))
, issuerPubKey(requireOptional(mpt.getPubKey(issuer), "Missing issuer public key"))
, auditorPubKey(auditor ? mpt.getPubKey(auditor->get()) : std::nullopt)
, prevSpending(requireOptional(
mpt.getDecryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
"Missing sender spending balance"))
, prevEncryptedSpending(requireOptional(
mpt.getEncryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
"Missing sender encrypted spending balance"))
, balanceCommitment(mpt.getPedersenCommitment(prevSpending, balanceBlindingFactor))
{
recipients.push_back({
.publicKey = Slice(senderPubKey),
.encryptedAmount = senderAmt,
});
recipients.push_back({
.publicKey = Slice(destPubKey),
.encryptedAmount = destAmt,
});
recipients.push_back({
.publicKey = Slice(issuerPubKey),
.encryptedAmount = issuerAmt,
});
if (auditor)
{
recipients.push_back({
.publicKey =
Slice(requireOptionalRef(auditorPubKey, "Missing auditor public key")),
.encryptedAmount =
requireOptionalRef(auditorAmt, "Missing auditor encrypted amount"),
});
}
}
// Generate proof with current account sequence
std::optional<Buffer>
generateProof(
test::jtx::MPTTester& mpt,
test::jtx::Env& env,
test::jtx::Account const& sender,
test::jtx::Account const& dest) const
{
auto const ctxHash = getSendContextHash(
sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), version);
return mpt.getConfidentialSendProof(
sender,
sendAmount,
recipients,
blindingFactor,
ctxHash,
{
.pedersenCommitment = amountCommitment,
.amt = sendAmount,
.encryptedAmt = senderAmt,
.blindingFactor = amountBlindingFactor,
},
{
.pedersenCommitment = balanceCommitment,
.amt = prevSpending,
.encryptedAmt = prevEncryptedSpending,
.blindingFactor = balanceBlindingFactor,
});
}
[[nodiscard]] test::jtx::MPTConfidentialSend
sendArgs(
test::jtx::Account const& sender,
test::jtx::Account const& dest,
Buffer const& proof,
std::optional<TER> err = std::nullopt) const
{
return {
.account = sender,
.dest = dest,
.amt = sendAmount,
.proof = strHex(proof),
.senderEncryptedAmt = senderAmt,
.destEncryptedAmt = destAmt,
.issuerEncryptedAmt = issuerAmt,
.auditorEncryptedAmt = auditorAmt,
.amountCommitment = amountCommitment,
.balanceCommitment = balanceCommitment,
.err = err,
};
}
};
// Helper that wraps the boilerplate setup: Env + MPT creation, funding, key
// generation, and seeding each holder with a confidential balance.
// The caller supplies the issuer and any number of holders.
struct ConfidentialEnv
{
// Per-holder configuration: the account, how much MPT to fund it
// with, and how much of that to convert to a confidential balance.
struct HolderInit
{
test::jtx::Account account;
std::uint64_t payAmount = 1000;
std::uint64_t convertAmount = 100;
};
test::jtx::MPTTester mpt;
ConfidentialEnv(
test::jtx::Env& env,
test::jtx::Account const& issuer,
std::vector<HolderInit> const& holders,
std::uint32_t flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
std::optional<test::jtx::Account> auditor = std::nullopt)
: mpt{env, issuer, {.holders = extractAccounts(holders), .auditor = auditor}}
{
mpt.create({.ownerCount = 1, .flags = flags});
for (auto const& h : holders)
{
mpt.authorize({.account = h.account});
if ((flags & tfMPTRequireAuth) != 0)
mpt.authorize({.account = issuer, .holder = h.account});
mpt.pay(issuer, h.account, h.payAmount);
}
mpt.generateKeyPair(issuer);
for (auto const& h : holders)
mpt.generateKeyPair(h.account);
if (auditor)
mpt.generateKeyPair(requireOptionalRef(auditor, "Missing auditor"));
mpt.set({
.account = issuer,
.issuerPubKey = mpt.getPubKey(issuer),
.auditorPubKey = auditor
? mpt.getPubKey(requireOptionalRef(auditor, "Missing auditor"))
: std::optional<Buffer>{},
});
for (auto const& h : holders)
{
mpt.convert({
.account = h.account,
.amt = h.convertAmount,
.holderPubKey = mpt.getPubKey(h.account),
});
mpt.mergeInbox({.account = h.account});
}
}
private:
static std::vector<test::jtx::Account>
extractAccounts(std::vector<HolderInit> const& holders)
{
std::vector<test::jtx::Account> accounts;
accounts.reserve(holders.size());
for (auto const& h : holders)
accounts.push_back(h.account);
return accounts;
}
};
// Set up an MPT environment suitable for batch testing.
// alice is issuer; bob has 'bobAmt' in confidential spending; carol has
// 'carolAmt' in confidential spending; dave is initialised with pubkey but
// zero spending/inbox.
static void
setupBatchEnv(
test::jtx::MPTTester& mpt,
test::jtx::Account const& alice,
test::jtx::Account const& bob,
test::jtx::Account const& carol,
test::jtx::Account const& dave,
std::uint64_t bobAmt,
std::uint64_t carolAmt)
{
using namespace test::jtx;
mpt.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
});
mpt.authorize({.account = bob});
mpt.authorize({.account = carol});
mpt.authorize({.account = dave});
if (bobAmt > 0)
mpt.pay(alice, bob, bobAmt);
if (carolAmt > 0)
mpt.pay(alice, carol, carolAmt);
mpt.generateKeyPair(alice);
mpt.generateKeyPair(bob);
mpt.generateKeyPair(carol);
mpt.generateKeyPair(dave);
mpt.set({
.account = alice,
.issuerPubKey = mpt.getPubKey(alice),
});
if (bobAmt > 0)
{
mpt.convert({
.account = bob,
.amt = bobAmt,
.holderPubKey = mpt.getPubKey(bob),
});
mpt.mergeInbox({.account = bob});
}
else
{
mpt.convert({
.account = bob,
.amt = 0,
.holderPubKey = mpt.getPubKey(bob),
});
}
if (carolAmt > 0)
{
mpt.convert({
.account = carol,
.amt = carolAmt,
.holderPubKey = mpt.getPubKey(carol),
});
mpt.mergeInbox({.account = carol});
}
else
{
mpt.convert({
.account = carol,
.amt = 0,
.holderPubKey = mpt.getPubKey(carol),
});
}
// dave: register pubkey only (0 spending/inbox)
mpt.convert({
.account = dave,
.amt = 0,
.holderPubKey = mpt.getPubKey(dave),
});
}
};
} // namespace xrpl
+34 -6
View File
@@ -14,18 +14,46 @@
#include <optional>
#include <utility>
/** Batch operations */
/** @brief Helpers for constructing Batch test transactions. */
namespace xrpl::test::jtx::batch {
/** Calculate Batch Fee. */
/**
* @brief Calculate the expected outer Batch transaction fee.
*
* @param env The test environment providing ledger fee settings.
* @param numSigners Number of outer transaction signers.
* @param txns Number of inner transactions in the batch.
* @return The expected Batch fee.
*/
XRPAmount
calcBatchFee(jtx::Env const& env, uint32_t const& numSigners, uint32_t const& txns = 0);
/** Batch. */
/**
* @brief Calculate the expected Batch fee when inner transactions are
* confidential MPT transactions.
*
* @param env The test environment providing ledger fee settings.
* @param numSigners Number of outer transaction signers.
* @param txns Number of confidential MPT inner transactions in the batch.
* @return The expected Batch fee including confidential transaction fee
* multipliers.
*/
XRPAmount
calcConfidentialBatchFee(jtx::Env const& env, uint32_t const& numSigners, uint32_t const& txns = 0);
/**
* @brief Build an outer Batch transaction JSON object.
*
* @param account The account submitting the outer Batch transaction.
* @param seq The sequence number for the outer Batch transaction.
* @param fee The fee to set on the outer Batch transaction.
* @param flags The transaction flags to set.
* @return The outer Batch transaction JSON object.
*/
json::Value
outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint32_t flags);
/** Adds a new Batch Txn on a JTx and autofills. */
/** @brief Adds an inner Batch transaction to a JTx and autofills it. */
class Inner
{
private:
@@ -75,7 +103,7 @@ public:
}
};
/** Set a batch signature on a JTx. */
/** @brief Sets the Batch transaction signers on a JTx. */
class Sig
{
public:
@@ -98,7 +126,7 @@ public:
operator()(Env&, JTx& jt) const;
};
/** Set a batch nested multi-signature on a JTx. */
/** @brief Sets a nested multi-signature for a Batch transaction on a JTx. */
class Msig
{
public:
+11
View File
@@ -11,6 +11,7 @@
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Batch.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -37,6 +38,16 @@ calcBatchFee(test::jtx::Env const& env, uint32_t const& numSigners, uint32_t con
return ((numSigners + 2) * feeDrops) + feeDrops * txns;
}
XRPAmount
calcConfidentialBatchFee(
test::jtx::Env const& env,
uint32_t const& numSigners,
uint32_t const& txns)
{
XRPAmount const feeDrops = env.current()->fees().base;
return ((numSigners + 2) * feeDrops) + feeDrops * (kConfidentialFeeMultiplier + 1) * txns;
}
// Batch.
json::Value
outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint32_t flags)
+1902 -29
View File
File diff suppressed because it is too large. Load diff
+17 -1
View File
@@ -13,6 +13,7 @@
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STParsedJSON.h>
@@ -57,7 +58,22 @@ fillFee(json::Value& jv, ReadView const& view)
{
if (jv.isMember(jss::Fee))
return;
jv[jss::Fee] = to_string(view.fees().base);
auto const base = view.fees().base;
// For confidential transactions, the fee is higher because confidential
// transaction processing is more expensive.
auto const txType = jv[jss::TransactionType].asString();
if (txType == jss::ConfidentialMPTConvert || txType == jss::ConfidentialMPTConvertBack ||
txType == jss::ConfidentialMPTSend || txType == jss::ConfidentialMPTMergeInbox ||
txType == jss::ConfidentialMPTClawback)
{
jv[jss::Fee] = to_string(base * (kConfidentialFeeMultiplier + 1));
}
else
{
jv[jss::Fee] = to_string(base);
}
}
void
+438 -4
View File
@@ -2,12 +2,21 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/delegate.h>
#include <test/jtx/owners.h>
#include <test/jtx/tag.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/txflags.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace xrpl::test::jtx {
@@ -15,7 +24,28 @@ class MPTTester;
auto const kMptDexFlags = tfMPTCanTrade | tfMPTCanTransfer;
// Check flags settings on MPT create
/**
* @brief Create a zero-initialized buffer for malformed cryptography test
* inputs.
*
* xrpl::Buffer(size) allocates uninitialized heap memory. Because CI runs unit
* tests sequentially in the same process, uninitialized memory can recycle
* valid secp256k1 keys or Pedersen commitments from earlier tests. Explicitly
* zeroing the buffer guarantees structural validation fails deterministically.
*
* @param size The number of zero bytes to allocate.
* @return A buffer containing size zero bytes.
*/
[[nodiscard]] inline Buffer
gMakeZeroBuffer(std::size_t size)
{
Buffer b(size);
if (size > 0)
std::memset(b.data(), 0, size);
return b;
}
/** @brief Test helper that checks MPT flag settings after creation. */
class MptFlags
{
private:
@@ -36,7 +66,7 @@ public:
operator()(Env& env) const;
};
// Check mptissuance or mptoken amount balances on payment
/** @brief Test helper that checks MPT issuance or holder balances. */
class MptBalance
{
private:
@@ -54,6 +84,7 @@ public:
operator()(Env& env) const;
};
/** @brief Test helper that accepts any condition supplied by a callback. */
class RequireAny
{
private:
@@ -70,6 +101,7 @@ public:
using Holders = std::vector<Account>;
/** @brief Arguments for building an MPTokenIssuanceCreate test transaction. */
struct MPTCreate
{
static inline std::vector<Account> allHolders = {};
@@ -93,9 +125,13 @@ struct MPTCreate
std::optional<TER> err = std::nullopt;
};
/** @brief Arguments for initializing funded MPT test accounts and issuance. */
struct MPTInit
{
// Default-initialized so designated-initializer call sites that omit
// `holders` don't trip GCC's -Werror=missing-field-initializers.
Holders holders = {}; // NOLINT(readability-redundant-member-init)
std::optional<Account> auditor = std::nullopt;
PrettyAmount const xrp = XRP(10'000);
PrettyAmount const xrpHolders = XRP(10'000);
bool fund = true;
@@ -105,11 +141,13 @@ struct MPTInit
};
static MPTInit const kMptInitNoFund{.fund = false};
/** @brief Full constructor arguments for MPTTester initialization. */
struct MPTInitDef
{
Env& env;
Account issuer;
Holders holders = {}; // NOLINT(readability-redundant-member-init)
std::optional<Account> auditor = std::nullopt;
std::uint16_t transferFee = 0;
std::optional<std::uint64_t> pay = std::nullopt;
std::uint32_t flags = kMptDexFlags;
@@ -121,6 +159,7 @@ struct MPTInitDef
std::optional<TER> err = std::nullopt;
};
/** @brief Arguments for building an MPTokenIssuanceDestroy test transaction. */
struct MPTDestroy
{
std::optional<Account> issuer = std::nullopt;
@@ -131,6 +170,7 @@ struct MPTDestroy
std::optional<TER> err = std::nullopt;
};
/** @brief Arguments for building an MPTokenAuthorize test transaction. */
struct MPTAuthorize
{
std::optional<Account> account = std::nullopt;
@@ -142,6 +182,7 @@ struct MPTAuthorize
std::optional<TER> err = std::nullopt;
};
/** @brief Arguments for building an MPTokenIssuanceSet test transaction. */
struct MPTSet
{
std::optional<Account> account = std::nullopt;
@@ -155,18 +196,215 @@ 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<std::uint32_t> ticketSeq = std::nullopt;
std::optional<TER> err = std::nullopt;
};
/** @brief Arguments for building a ConfidentialMPTConvert test transaction. */
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<Account> delegate = std::nullopt;
std::optional<std::uint32_t> ticketSeq = 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<XRPAmount> fee = std::nullopt;
std::optional<TER> err = std::nullopt;
};
/** @brief Arguments for building a ConfidentialMPTMergeInbox test transaction. */
struct MPTMergeInbox
{
std::optional<Account> account = std::nullopt;
std::optional<MPTID> id = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<std::uint32_t> ticketSeq = 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<XRPAmount> fee = std::nullopt;
std::optional<TER> err = std::nullopt;
};
/** @brief Arguments for building a ConfidentialMPTSend test transaction. */
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<bool> fillAuditorEncryptedAmt = true;
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<Account> delegate = std::nullopt;
std::optional<std::uint32_t> destinationTag = std::nullopt;
std::optional<std::uint32_t> ticketSeq = 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<XRPAmount> fee = std::nullopt;
std::optional<TER> err = std::nullopt;
};
/** @brief Arguments for building a ConfidentialMPTConvertBack test transaction. */
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<Account> delegate = std::nullopt;
std::optional<std::uint32_t> ticketSeq = 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<XRPAmount> fee = std::nullopt;
std::optional<TER> err = std::nullopt;
};
/** @brief Arguments for building a ConfidentialMPTClawback test transaction. */
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<Account> delegate = std::nullopt;
std::optional<std::uint32_t> ticketSeq = 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<XRPAmount> fee = std::nullopt;
std::optional<TER> err = std::nullopt;
};
/**
* @brief Stores the parameters that are exclusively used to generate a
* Pedersen linkage proof.
*/
struct PedersenProofParams
{
/** @brief The Pedersen commitment used by the proof. */
Buffer const pedersenCommitment;
/** @brief Either the spending balance or the value being transferred. */
uint64_t const amt;
/** @brief The encrypted amount linked to the Pedersen commitment. */
Buffer const encryptedAmt;
/** @brief The blinding factor used to create the Pedersen commitment. */
Buffer const blindingFactor;
};
/**
* @brief When building multiple confidential sends from the same account inside a
* single batch transaction, pass this state to the transaction builder for
* each subsequent send so that its proof references the post previous-send
* encrypted balance rather than the stale pre-send ledger state.
*
* The fields mirror what the ledger will contain after the preceding send's
* doApply() completes:
* encSpending = homomorphicSubtract(prevEncSpending, senderEncAmt)
* version = prevVersion + 1
*/
struct ConfidentialSendChainState
{
/** @brief Decrypted spending balance after the previous send. */
std::uint64_t spending;
/** @brief Encrypted spending balance after the previous send. */
Buffer encSpending;
/** @brief sfConfidentialBalanceVersion after the previous send. */
std::uint32_t version;
};
/**
* @brief Use this when building a second (or later) confidential send from the same
* account in the same batch. Pass the state to the chain aware
* transaction builder so that the next proof is constructed against the
* correct post-send encrypted balance and version.
*
* @param currentSpending Decrypted spending balance before the send.
* @param currentEncSpending sfConfidentialBalanceSpending before the send.
* @param currentVersion sfConfidentialBalanceVersion before the send.
* @param sendAmt Plaintext amount being sent.
* @param senderEncAmt sfSenderEncryptedAmount from the send transaction.
* @return The predicted chain state, or std::nullopt if homomorphic
* subtraction fails.
*/
std::optional<ConfidentialSendChainState>
computeNextSendChainState(
std::uint64_t currentSpending,
Slice const& currentEncSpending,
std::uint32_t currentVersion,
std::uint64_t sendAmt,
Slice const& senderEncAmt);
/**
* @brief Test helper for creating, mutating, and asserting MPT and confidential
* MPT ledger state.
*/
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 class EncryptedBalanceType {
IssuerEncryptedBalance,
HolderEncryptedInbox,
HolderEncryptedSpending,
AuditorEncryptedBalance,
};
static constexpr auto issuerEncryptedBalance = EncryptedBalanceType::IssuerEncryptedBalance;
static constexpr auto holderEncryptedInbox = EncryptedBalanceType::HolderEncryptedInbox;
static constexpr auto holderEncryptedSpending = EncryptedBalanceType::HolderEncryptedSpending;
static constexpr auto auditorEncryptedBalance = EncryptedBalanceType::AuditorEncryptedBalance;
MPTTester(Env& env, Account issuer, MPTInit const& constr = {});
MPTTester(MPTInitDef const& constr);
MPTTester(
@@ -204,6 +442,83 @@ public:
static json::Value
setJV(MPTSet const& set = {});
void
convert(MPTConvert const& arg = MPTConvert{});
/**
* @brief Build a confidential convert JV without submitting it.
*
* @param arg Transaction builder arguments.
* @param seq Inner transaction sequence used in the Schnorr proof context
* hash.
* @return The transaction JSON object.
*/
json::Value
convertJV(MPTConvert const& arg, std::uint32_t seq);
void
mergeInbox(MPTMergeInbox const& arg = MPTMergeInbox{});
[[nodiscard]] json::Value
mergeInboxJV(MPTMergeInbox const& arg = MPTMergeInbox{}) const;
void
send(MPTConfidentialSend const& arg = MPTConfidentialSend{});
/**
* @brief Build a confidential send JV.
*
* When chain is provided, the sender's proof parameters are taken from it
* instead of the ledger, enabling proof generation for a second or later
* send from the same account inside a single batch.
*
* @param arg Transaction builder arguments.
* @param seq Inner transaction sequence used in the proof context hash.
* @param chain Optional predicted sender state from a previous batched
* send.
* @return The transaction JSON object.
*/
json::Value
sendJV(
MPTConfidentialSend const& arg,
std::uint32_t seq,
std::optional<ConfidentialSendChainState> chain = std::nullopt);
/**
* @brief Compute the projected sender state after a confidential send in a
* batch.
*
* Each confidential send requires a ZK proof that the sender's spending
* balance covers the transfer. In a batch, the second and later sends from
* the same sender need proofs built against the updated spending balance.
*
* @param sender The sender whose post-send state is being predicted.
* @param sendAmt The plaintext amount sent by the transaction.
* @param jv The confidential send transaction JSON object.
* @return The predicted sender state after applying the send.
*/
[[nodiscard]] ConfidentialSendChainState
chainAfterSend(Account const& sender, std::uint64_t sendAmt, json::Value const& jv) const;
void
convertBack(MPTConvertBack const& arg = MPTConvertBack{});
/**
* @brief Build a confidential convertBack JV without submitting it.
*
* Reads the current encrypted spending balance and version from the ledger,
* so call this before the batch is submitted.
*
* @param arg Transaction builder arguments.
* @param seq Inner transaction sequence used in the proof context hash.
* @return The transaction JSON object.
*/
json::Value
convertBackJV(MPTConvertBack const& arg, std::uint32_t seq);
void
confidentialClaw(MPTConfidentialClawback const& arg = MPTConfidentialClawback{});
[[nodiscard]] bool
checkDomainID(std::optional<uint256> expected) const;
@@ -213,6 +528,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;
@@ -265,6 +583,13 @@ public:
[[nodiscard]] std::int64_t
getBalance(Account const& account) const;
[[nodiscard]] std::int64_t
getIssuanceConfidentialBalance() const;
[[nodiscard]] std::optional<Buffer>
getEncryptedBalance(Account const& account, EncryptedBalanceType option = holderEncryptedInbox)
const;
MPT
operator[](std::string const& name) const;
@@ -273,6 +598,60 @@ public:
operator Asset() const;
void
generateKeyPair(Account const& account);
[[nodiscard]] std::optional<Buffer>
getPubKey(Account const& account) const;
[[nodiscard]] std::optional<Buffer>
getPrivKey(Account const& account) const;
[[nodiscard]] Buffer
encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor) const;
[[nodiscard]] std::optional<uint64_t>
decryptAmount(Account const& account, Buffer const& amt) const;
[[nodiscard]] std::optional<uint64_t>
getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const;
[[nodiscard]] std::optional<std::int64_t>
getIssuanceOutstandingBalance() const;
[[nodiscard]] std::optional<Buffer>
getClawbackProof(
Account const& holder,
std::uint64_t amount,
Buffer const& privateKey,
uint256 const& txHash) const;
[[nodiscard]] std::optional<Buffer>
getSchnorrProof(Account const& account, uint256 const& ctxHash) const;
[[nodiscard]] std::optional<Buffer>
getConfidentialSendProof(
Account const& sender,
std::uint64_t const amount,
std::vector<ConfidentialRecipient> const& recipients,
Slice const& blindingFactor,
uint256 const& contextHash,
PedersenProofParams const& amountParams,
PedersenProofParams const& balanceParams) const;
[[nodiscard]] Buffer
getConvertBackProof(
Account const& holder,
std::uint64_t const amount,
uint256 const& contextHash,
PedersenProofParams const& pcParams) const;
[[nodiscard]] std::uint32_t
getMPTokenVersion(Account const account) const;
static Buffer
getPedersenCommitment(std::uint64_t const amount, Buffer const& pedersenBlindingFactor);
friend BookSpec
operator~(MPTTester const& mpt)
{
@@ -288,9 +667,54 @@ private:
template <typename A>
TER
submit(A const& arg, json::Value const& jv)
submit(A const& arg, json::Value jv)
{
env_(jv, Txflags(arg.flags.value_or(0)), Ter(arg.err.value_or(tesSUCCESS)));
auto const expectedFlags = Txflags(arg.flags.value_or(0));
auto const expectedTer = Ter(arg.err.value_or(tesSUCCESS));
if constexpr (requires { arg.fee; })
{
if (arg.fee)
jv[jss::Fee] = to_string(*arg.fee);
}
std::optional<std::uint32_t> ticketSeq;
if constexpr (requires { arg.ticketSeq; })
ticketSeq = arg.ticketSeq;
std::optional<Account> delegateAcct;
if constexpr (requires { arg.delegate; })
delegateAcct = arg.delegate;
std::optional<std::uint32_t> dstTag;
if constexpr (requires { arg.destinationTag; })
dstTag = arg.destinationTag;
if (ticketSeq && delegateAcct)
{
env_(
jv,
expectedFlags,
expectedTer,
ticket::Use(*ticketSeq),
delegate::As(*delegateAcct));
}
else if (ticketSeq)
{
env_(jv, expectedFlags, expectedTer, ticket::Use(*ticketSeq));
}
else if (delegateAcct)
{
env_(jv, expectedFlags, expectedTer, delegate::As(*delegateAcct));
}
else if (dstTag)
{
env_(jv, expectedFlags, expectedTer, Dtag(*dstTag));
}
else
{
env_(jv, expectedFlags, expectedTer);
}
auto const err = env_.ter();
if (close_)
env_.close();
@@ -309,6 +733,16 @@ private:
[[nodiscard]] 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 xrpl::test::jtx
@@ -34,6 +34,9 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
auto const domainIDValue = canonical_UINT256();
auto const mutableFlagsValue = canonical_UINT32();
auto const referenceHoldingValue = canonical_UINT256();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
auto const confidentialOutstandingAmountValue = canonical_UINT64();
MPTokenIssuanceBuilder builder{
issuerValue,
@@ -52,6 +55,9 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
builder.setDomainID(domainIDValue);
builder.setMutableFlags(mutableFlagsValue);
builder.setReferenceHolding(referenceHoldingValue);
builder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
builder.setConfidentialOutstandingAmount(confidentialOutstandingAmountValue);
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
@@ -162,6 +168,30 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasReferenceHolding());
}
{
auto const& expected = issuerEncryptionKeyValue;
auto const actualOpt = entry.getIssuerEncryptionKey();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfIssuerEncryptionKey");
EXPECT_TRUE(entry.hasIssuerEncryptionKey());
}
{
auto const& expected = auditorEncryptionKeyValue;
auto const actualOpt = entry.getAuditorEncryptionKey();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey");
EXPECT_TRUE(entry.hasAuditorEncryptionKey());
}
{
auto const& expected = confidentialOutstandingAmountValue;
auto const actualOpt = entry.getConfidentialOutstandingAmount();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfConfidentialOutstandingAmount");
EXPECT_TRUE(entry.hasConfidentialOutstandingAmount());
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
@@ -189,6 +219,9 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
auto const domainIDValue = canonical_UINT256();
auto const mutableFlagsValue = canonical_UINT32();
auto const referenceHoldingValue = canonical_UINT256();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
auto const confidentialOutstandingAmountValue = canonical_UINT64();
auto sle = std::make_shared<SLE>(MPTokenIssuance::entryType, index);
@@ -206,6 +239,9 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
sle->at(sfDomainID) = domainIDValue;
sle->at(sfMutableFlags) = mutableFlagsValue;
sle->at(sfReferenceHolding) = referenceHoldingValue;
sle->at(sfIssuerEncryptionKey) = issuerEncryptionKeyValue;
sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue;
sle->at(sfConfidentialOutstandingAmount) = confidentialOutstandingAmountValue;
MPTokenIssuanceBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
@@ -380,6 +416,45 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfReferenceHolding");
}
{
auto const& expected = issuerEncryptionKeyValue;
auto const fromSleOpt = entryFromSle.getIssuerEncryptionKey();
auto const fromBuilderOpt = entryFromBuilder.getIssuerEncryptionKey();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfIssuerEncryptionKey");
expectEqualField(expected, *fromBuilderOpt, "sfIssuerEncryptionKey");
}
{
auto const& expected = auditorEncryptionKeyValue;
auto const fromSleOpt = entryFromSle.getAuditorEncryptionKey();
auto const fromBuilderOpt = entryFromBuilder.getAuditorEncryptionKey();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfAuditorEncryptionKey");
expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptionKey");
}
{
auto const& expected = confidentialOutstandingAmountValue;
auto const fromSleOpt = entryFromSle.getConfidentialOutstandingAmount();
auto const fromBuilderOpt = entryFromBuilder.getConfidentialOutstandingAmount();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfConfidentialOutstandingAmount");
expectEqualField(expected, *fromBuilderOpt, "sfConfidentialOutstandingAmount");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
@@ -460,5 +535,11 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getMutableFlags().has_value());
EXPECT_FALSE(entry.hasReferenceHolding());
EXPECT_FALSE(entry.getReferenceHolding().has_value());
EXPECT_FALSE(entry.hasIssuerEncryptionKey());
EXPECT_FALSE(entry.getIssuerEncryptionKey().has_value());
EXPECT_FALSE(entry.hasAuditorEncryptionKey());
EXPECT_FALSE(entry.getAuditorEncryptionKey().has_value());
EXPECT_FALSE(entry.hasConfidentialOutstandingAmount());
EXPECT_FALSE(entry.getConfidentialOutstandingAmount().has_value());
}
}
@@ -27,6 +27,12 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
auto const ownerNodeValue = canonical_UINT64();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const confidentialBalanceInboxValue = canonical_VL();
auto const confidentialBalanceSpendingValue = canonical_VL();
auto const confidentialBalanceVersionValue = canonical_UINT32();
auto const issuerEncryptedBalanceValue = canonical_VL();
auto const auditorEncryptedBalanceValue = canonical_VL();
auto const holderEncryptionKeyValue = canonical_VL();
MPTokenBuilder builder{
accountValue,
@@ -38,6 +44,12 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
builder.setMPTAmount(mPTAmountValue);
builder.setLockedAmount(lockedAmountValue);
builder.setConfidentialBalanceInbox(confidentialBalanceInboxValue);
builder.setConfidentialBalanceSpending(confidentialBalanceSpendingValue);
builder.setConfidentialBalanceVersion(confidentialBalanceVersionValue);
builder.setIssuerEncryptedBalance(issuerEncryptedBalanceValue);
builder.setAuditorEncryptedBalance(auditorEncryptedBalanceValue);
builder.setHolderEncryptionKey(holderEncryptionKeyValue);
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
@@ -94,6 +106,54 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasLockedAmount());
}
{
auto const& expected = confidentialBalanceInboxValue;
auto const actualOpt = entry.getConfidentialBalanceInbox();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceInbox");
EXPECT_TRUE(entry.hasConfidentialBalanceInbox());
}
{
auto const& expected = confidentialBalanceSpendingValue;
auto const actualOpt = entry.getConfidentialBalanceSpending();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceSpending");
EXPECT_TRUE(entry.hasConfidentialBalanceSpending());
}
{
auto const& expected = confidentialBalanceVersionValue;
auto const actualOpt = entry.getConfidentialBalanceVersion();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceVersion");
EXPECT_TRUE(entry.hasConfidentialBalanceVersion());
}
{
auto const& expected = issuerEncryptedBalanceValue;
auto const actualOpt = entry.getIssuerEncryptedBalance();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfIssuerEncryptedBalance");
EXPECT_TRUE(entry.hasIssuerEncryptedBalance());
}
{
auto const& expected = auditorEncryptedBalanceValue;
auto const actualOpt = entry.getAuditorEncryptedBalance();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfAuditorEncryptedBalance");
EXPECT_TRUE(entry.hasAuditorEncryptedBalance());
}
{
auto const& expected = holderEncryptionKeyValue;
auto const actualOpt = entry.getHolderEncryptionKey();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey");
EXPECT_TRUE(entry.hasHolderEncryptionKey());
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
@@ -114,6 +174,12 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
auto const ownerNodeValue = canonical_UINT64();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const confidentialBalanceInboxValue = canonical_VL();
auto const confidentialBalanceSpendingValue = canonical_VL();
auto const confidentialBalanceVersionValue = canonical_UINT32();
auto const issuerEncryptedBalanceValue = canonical_VL();
auto const auditorEncryptedBalanceValue = canonical_VL();
auto const holderEncryptionKeyValue = canonical_VL();
auto sle = std::make_shared<SLE>(MPToken::entryType, index);
@@ -124,6 +190,12 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
sle->at(sfOwnerNode) = ownerNodeValue;
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
sle->at(sfConfidentialBalanceInbox) = confidentialBalanceInboxValue;
sle->at(sfConfidentialBalanceSpending) = confidentialBalanceSpendingValue;
sle->at(sfConfidentialBalanceVersion) = confidentialBalanceVersionValue;
sle->at(sfIssuerEncryptedBalance) = issuerEncryptedBalanceValue;
sle->at(sfAuditorEncryptedBalance) = auditorEncryptedBalanceValue;
sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue;
MPTokenBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
@@ -210,6 +282,84 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfLockedAmount");
}
{
auto const& expected = confidentialBalanceInboxValue;
auto const fromSleOpt = entryFromSle.getConfidentialBalanceInbox();
auto const fromBuilderOpt = entryFromBuilder.getConfidentialBalanceInbox();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfConfidentialBalanceInbox");
expectEqualField(expected, *fromBuilderOpt, "sfConfidentialBalanceInbox");
}
{
auto const& expected = confidentialBalanceSpendingValue;
auto const fromSleOpt = entryFromSle.getConfidentialBalanceSpending();
auto const fromBuilderOpt = entryFromBuilder.getConfidentialBalanceSpending();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfConfidentialBalanceSpending");
expectEqualField(expected, *fromBuilderOpt, "sfConfidentialBalanceSpending");
}
{
auto const& expected = confidentialBalanceVersionValue;
auto const fromSleOpt = entryFromSle.getConfidentialBalanceVersion();
auto const fromBuilderOpt = entryFromBuilder.getConfidentialBalanceVersion();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfConfidentialBalanceVersion");
expectEqualField(expected, *fromBuilderOpt, "sfConfidentialBalanceVersion");
}
{
auto const& expected = issuerEncryptedBalanceValue;
auto const fromSleOpt = entryFromSle.getIssuerEncryptedBalance();
auto const fromBuilderOpt = entryFromBuilder.getIssuerEncryptedBalance();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfIssuerEncryptedBalance");
expectEqualField(expected, *fromBuilderOpt, "sfIssuerEncryptedBalance");
}
{
auto const& expected = auditorEncryptedBalanceValue;
auto const fromSleOpt = entryFromSle.getAuditorEncryptedBalance();
auto const fromBuilderOpt = entryFromBuilder.getAuditorEncryptedBalance();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfAuditorEncryptedBalance");
expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptedBalance");
}
{
auto const& expected = holderEncryptionKeyValue;
auto const fromSleOpt = entryFromSle.getHolderEncryptionKey();
auto const fromBuilderOpt = entryFromBuilder.getHolderEncryptionKey();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfHolderEncryptionKey");
expectEqualField(expected, *fromBuilderOpt, "sfHolderEncryptionKey");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
@@ -276,5 +426,17 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getMPTAmount().has_value());
EXPECT_FALSE(entry.hasLockedAmount());
EXPECT_FALSE(entry.getLockedAmount().has_value());
EXPECT_FALSE(entry.hasConfidentialBalanceInbox());
EXPECT_FALSE(entry.getConfidentialBalanceInbox().has_value());
EXPECT_FALSE(entry.hasConfidentialBalanceSpending());
EXPECT_FALSE(entry.getConfidentialBalanceSpending().has_value());
EXPECT_FALSE(entry.hasConfidentialBalanceVersion());
EXPECT_FALSE(entry.getConfidentialBalanceVersion().has_value());
EXPECT_FALSE(entry.hasIssuerEncryptedBalance());
EXPECT_FALSE(entry.getIssuerEncryptedBalance().has_value());
EXPECT_FALSE(entry.hasAuditorEncryptedBalance());
EXPECT_FALSE(entry.getAuditorEncryptedBalance().has_value());
EXPECT_FALSE(entry.hasHolderEncryptionKey());
EXPECT_FALSE(entry.getHolderEncryptionKey().has_value());
}
}
@@ -0,0 +1,194 @@
// Auto-generated unit tests for transaction ConfidentialMPTClawback
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsConfidentialMPTClawbackTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTClawback"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const holderValue = canonical_ACCOUNT();
auto const mPTAmountValue = canonical_UINT64();
auto const zKProofValue = canonical_VL();
ConfidentialMPTClawbackBuilder builder{
accountValue,
mPTokenIssuanceIDValue,
holderValue,
mPTAmountValue,
zKProofValue,
sequenceValue,
feeValue
};
// Set optional fields
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = tx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
{
auto const& expected = holderValue;
auto const actual = tx.getHolder();
expectEqualField(expected, actual, "sfHolder");
}
{
auto const& expected = mPTAmountValue;
auto const actual = tx.getMPTAmount();
expectEqualField(expected, actual, "sfMPTAmount");
}
{
auto const& expected = zKProofValue;
auto const actual = tx.getZKProof();
expectEqualField(expected, actual, "sfZKProof");
}
// Verify optional fields
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsConfidentialMPTClawbackTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTClawbackFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const holderValue = canonical_ACCOUNT();
auto const mPTAmountValue = canonical_UINT64();
auto const zKProofValue = canonical_VL();
// Build an initial transaction
ConfidentialMPTClawbackBuilder initialBuilder{
accountValue,
mPTokenIssuanceIDValue,
holderValue,
mPTAmountValue,
zKProofValue,
sequenceValue,
feeValue
};
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
ConfidentialMPTClawbackBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = rebuiltTx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
{
auto const& expected = holderValue;
auto const actual = rebuiltTx.getHolder();
expectEqualField(expected, actual, "sfHolder");
}
{
auto const& expected = mPTAmountValue;
auto const actual = rebuiltTx.getMPTAmount();
expectEqualField(expected, actual, "sfMPTAmount");
}
{
auto const& expected = zKProofValue;
auto const actual = rebuiltTx.getZKProof();
expectEqualField(expected, actual, "sfZKProof");
}
// Verify optional fields
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTClawbackTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTClawback{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTClawbackTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTClawbackBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
}
@@ -0,0 +1,303 @@
// Auto-generated unit tests for transaction ConfidentialMPTConvertBack
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsConfidentialMPTConvertBackTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertBack"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const mPTAmountValue = canonical_UINT64();
auto const holderEncryptedAmountValue = canonical_VL();
auto const issuerEncryptedAmountValue = canonical_VL();
auto const auditorEncryptedAmountValue = canonical_VL();
auto const blindingFactorValue = canonical_UINT256();
auto const zKProofValue = canonical_VL();
auto const balanceCommitmentValue = canonical_VL();
ConfidentialMPTConvertBackBuilder builder{
accountValue,
mPTokenIssuanceIDValue,
mPTAmountValue,
holderEncryptedAmountValue,
issuerEncryptedAmountValue,
blindingFactorValue,
zKProofValue,
balanceCommitmentValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = tx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
{
auto const& expected = mPTAmountValue;
auto const actual = tx.getMPTAmount();
expectEqualField(expected, actual, "sfMPTAmount");
}
{
auto const& expected = holderEncryptedAmountValue;
auto const actual = tx.getHolderEncryptedAmount();
expectEqualField(expected, actual, "sfHolderEncryptedAmount");
}
{
auto const& expected = issuerEncryptedAmountValue;
auto const actual = tx.getIssuerEncryptedAmount();
expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
}
{
auto const& expected = blindingFactorValue;
auto const actual = tx.getBlindingFactor();
expectEqualField(expected, actual, "sfBlindingFactor");
}
{
auto const& expected = zKProofValue;
auto const actual = tx.getZKProof();
expectEqualField(expected, actual, "sfZKProof");
}
{
auto const& expected = balanceCommitmentValue;
auto const actual = tx.getBalanceCommitment();
expectEqualField(expected, actual, "sfBalanceCommitment");
}
// Verify optional fields
{
auto const& expected = auditorEncryptedAmountValue;
auto const actualOpt = tx.getAuditorEncryptedAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
EXPECT_TRUE(tx.hasAuditorEncryptedAmount());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsConfidentialMPTConvertBackTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertBackFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const mPTAmountValue = canonical_UINT64();
auto const holderEncryptedAmountValue = canonical_VL();
auto const issuerEncryptedAmountValue = canonical_VL();
auto const auditorEncryptedAmountValue = canonical_VL();
auto const blindingFactorValue = canonical_UINT256();
auto const zKProofValue = canonical_VL();
auto const balanceCommitmentValue = canonical_VL();
// Build an initial transaction
ConfidentialMPTConvertBackBuilder initialBuilder{
accountValue,
mPTokenIssuanceIDValue,
mPTAmountValue,
holderEncryptedAmountValue,
issuerEncryptedAmountValue,
blindingFactorValue,
zKProofValue,
balanceCommitmentValue,
sequenceValue,
feeValue
};
initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
ConfidentialMPTConvertBackBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = rebuiltTx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
{
auto const& expected = mPTAmountValue;
auto const actual = rebuiltTx.getMPTAmount();
expectEqualField(expected, actual, "sfMPTAmount");
}
{
auto const& expected = holderEncryptedAmountValue;
auto const actual = rebuiltTx.getHolderEncryptedAmount();
expectEqualField(expected, actual, "sfHolderEncryptedAmount");
}
{
auto const& expected = issuerEncryptedAmountValue;
auto const actual = rebuiltTx.getIssuerEncryptedAmount();
expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
}
{
auto const& expected = blindingFactorValue;
auto const actual = rebuiltTx.getBlindingFactor();
expectEqualField(expected, actual, "sfBlindingFactor");
}
{
auto const& expected = zKProofValue;
auto const actual = rebuiltTx.getZKProof();
expectEqualField(expected, actual, "sfZKProof");
}
{
auto const& expected = balanceCommitmentValue;
auto const actual = rebuiltTx.getBalanceCommitment();
expectEqualField(expected, actual, "sfBalanceCommitment");
}
// Verify optional fields
{
auto const& expected = auditorEncryptedAmountValue;
auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTConvertBackTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTConvertBack{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTConvertBackTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTConvertBackBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsConfidentialMPTConvertBackTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertBackNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const mPTAmountValue = canonical_UINT64();
auto const holderEncryptedAmountValue = canonical_VL();
auto const issuerEncryptedAmountValue = canonical_VL();
auto const blindingFactorValue = canonical_UINT256();
auto const zKProofValue = canonical_VL();
auto const balanceCommitmentValue = canonical_VL();
ConfidentialMPTConvertBackBuilder builder{
accountValue,
mPTokenIssuanceIDValue,
mPTAmountValue,
holderEncryptedAmountValue,
issuerEncryptedAmountValue,
blindingFactorValue,
zKProofValue,
balanceCommitmentValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasAuditorEncryptedAmount());
EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value());
}
}
@@ -0,0 +1,309 @@
// Auto-generated unit tests for transaction ConfidentialMPTConvert
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsConfidentialMPTConvertTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvert"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const mPTAmountValue = canonical_UINT64();
auto const holderEncryptionKeyValue = canonical_VL();
auto const holderEncryptedAmountValue = canonical_VL();
auto const issuerEncryptedAmountValue = canonical_VL();
auto const auditorEncryptedAmountValue = canonical_VL();
auto const blindingFactorValue = canonical_UINT256();
auto const zKProofValue = canonical_VL();
ConfidentialMPTConvertBuilder builder{
accountValue,
mPTokenIssuanceIDValue,
mPTAmountValue,
holderEncryptedAmountValue,
issuerEncryptedAmountValue,
blindingFactorValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setHolderEncryptionKey(holderEncryptionKeyValue);
builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
builder.setZKProof(zKProofValue);
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = tx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
{
auto const& expected = mPTAmountValue;
auto const actual = tx.getMPTAmount();
expectEqualField(expected, actual, "sfMPTAmount");
}
{
auto const& expected = holderEncryptedAmountValue;
auto const actual = tx.getHolderEncryptedAmount();
expectEqualField(expected, actual, "sfHolderEncryptedAmount");
}
{
auto const& expected = issuerEncryptedAmountValue;
auto const actual = tx.getIssuerEncryptedAmount();
expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
}
{
auto const& expected = blindingFactorValue;
auto const actual = tx.getBlindingFactor();
expectEqualField(expected, actual, "sfBlindingFactor");
}
// Verify optional fields
{
auto const& expected = holderEncryptionKeyValue;
auto const actualOpt = tx.getHolderEncryptionKey();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolderEncryptionKey should be present";
expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey");
EXPECT_TRUE(tx.hasHolderEncryptionKey());
}
{
auto const& expected = auditorEncryptedAmountValue;
auto const actualOpt = tx.getAuditorEncryptedAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
EXPECT_TRUE(tx.hasAuditorEncryptedAmount());
}
{
auto const& expected = zKProofValue;
auto const actualOpt = tx.getZKProof();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfZKProof should be present";
expectEqualField(expected, *actualOpt, "sfZKProof");
EXPECT_TRUE(tx.hasZKProof());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsConfidentialMPTConvertTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const mPTAmountValue = canonical_UINT64();
auto const holderEncryptionKeyValue = canonical_VL();
auto const holderEncryptedAmountValue = canonical_VL();
auto const issuerEncryptedAmountValue = canonical_VL();
auto const auditorEncryptedAmountValue = canonical_VL();
auto const blindingFactorValue = canonical_UINT256();
auto const zKProofValue = canonical_VL();
// Build an initial transaction
ConfidentialMPTConvertBuilder initialBuilder{
accountValue,
mPTokenIssuanceIDValue,
mPTAmountValue,
holderEncryptedAmountValue,
issuerEncryptedAmountValue,
blindingFactorValue,
sequenceValue,
feeValue
};
initialBuilder.setHolderEncryptionKey(holderEncryptionKeyValue);
initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
initialBuilder.setZKProof(zKProofValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
ConfidentialMPTConvertBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = rebuiltTx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
{
auto const& expected = mPTAmountValue;
auto const actual = rebuiltTx.getMPTAmount();
expectEqualField(expected, actual, "sfMPTAmount");
}
{
auto const& expected = holderEncryptedAmountValue;
auto const actual = rebuiltTx.getHolderEncryptedAmount();
expectEqualField(expected, actual, "sfHolderEncryptedAmount");
}
{
auto const& expected = issuerEncryptedAmountValue;
auto const actual = rebuiltTx.getIssuerEncryptedAmount();
expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
}
{
auto const& expected = blindingFactorValue;
auto const actual = rebuiltTx.getBlindingFactor();
expectEqualField(expected, actual, "sfBlindingFactor");
}
// Verify optional fields
{
auto const& expected = holderEncryptionKeyValue;
auto const actualOpt = rebuiltTx.getHolderEncryptionKey();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolderEncryptionKey should be present";
expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey");
}
{
auto const& expected = auditorEncryptedAmountValue;
auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
}
{
auto const& expected = zKProofValue;
auto const actualOpt = rebuiltTx.getZKProof();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfZKProof should be present";
expectEqualField(expected, *actualOpt, "sfZKProof");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTConvertTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTConvert{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTConvertTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTConvertBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsConfidentialMPTConvertTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const mPTAmountValue = canonical_UINT64();
auto const holderEncryptedAmountValue = canonical_VL();
auto const issuerEncryptedAmountValue = canonical_VL();
auto const blindingFactorValue = canonical_UINT256();
ConfidentialMPTConvertBuilder builder{
accountValue,
mPTokenIssuanceIDValue,
mPTAmountValue,
holderEncryptedAmountValue,
issuerEncryptedAmountValue,
blindingFactorValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasHolderEncryptionKey());
EXPECT_FALSE(tx.getHolderEncryptionKey().has_value());
EXPECT_FALSE(tx.hasAuditorEncryptedAmount());
EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value());
EXPECT_FALSE(tx.hasZKProof());
EXPECT_FALSE(tx.getZKProof().has_value());
}
}
@@ -0,0 +1,146 @@
// Auto-generated unit tests for transaction ConfidentialMPTMergeInbox
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsConfidentialMPTMergeInboxTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMergeInbox"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
ConfidentialMPTMergeInboxBuilder builder{
accountValue,
mPTokenIssuanceIDValue,
sequenceValue,
feeValue
};
// Set optional fields
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = tx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
// Verify optional fields
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsConfidentialMPTMergeInboxTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMergeInboxFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
// Build an initial transaction
ConfidentialMPTMergeInboxBuilder initialBuilder{
accountValue,
mPTokenIssuanceIDValue,
sequenceValue,
feeValue
};
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
ConfidentialMPTMergeInboxBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = rebuiltTx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
// Verify optional fields
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTMergeInboxTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTMergeInbox{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTMergeInboxTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTMergeInboxBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
}
@@ -0,0 +1,363 @@
// Auto-generated unit tests for transaction ConfidentialMPTSend
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsConfidentialMPTSendTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTSend"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const destinationValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const senderEncryptedAmountValue = canonical_VL();
auto const destinationEncryptedAmountValue = canonical_VL();
auto const issuerEncryptedAmountValue = canonical_VL();
auto const auditorEncryptedAmountValue = canonical_VL();
auto const zKProofValue = canonical_VL();
auto const amountCommitmentValue = canonical_VL();
auto const balanceCommitmentValue = canonical_VL();
auto const credentialIDsValue = canonical_VECTOR256();
ConfidentialMPTSendBuilder builder{
accountValue,
mPTokenIssuanceIDValue,
destinationValue,
senderEncryptedAmountValue,
destinationEncryptedAmountValue,
issuerEncryptedAmountValue,
zKProofValue,
amountCommitmentValue,
balanceCommitmentValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setDestinationTag(destinationTagValue);
builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
builder.setCredentialIDs(credentialIDsValue);
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = tx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
{
auto const& expected = destinationValue;
auto const actual = tx.getDestination();
expectEqualField(expected, actual, "sfDestination");
}
{
auto const& expected = senderEncryptedAmountValue;
auto const actual = tx.getSenderEncryptedAmount();
expectEqualField(expected, actual, "sfSenderEncryptedAmount");
}
{
auto const& expected = destinationEncryptedAmountValue;
auto const actual = tx.getDestinationEncryptedAmount();
expectEqualField(expected, actual, "sfDestinationEncryptedAmount");
}
{
auto const& expected = issuerEncryptedAmountValue;
auto const actual = tx.getIssuerEncryptedAmount();
expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
}
{
auto const& expected = zKProofValue;
auto const actual = tx.getZKProof();
expectEqualField(expected, actual, "sfZKProof");
}
{
auto const& expected = amountCommitmentValue;
auto const actual = tx.getAmountCommitment();
expectEqualField(expected, actual, "sfAmountCommitment");
}
{
auto const& expected = balanceCommitmentValue;
auto const actual = tx.getBalanceCommitment();
expectEqualField(expected, actual, "sfBalanceCommitment");
}
// Verify optional fields
{
auto const& expected = destinationTagValue;
auto const actualOpt = tx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(tx.hasDestinationTag());
}
{
auto const& expected = auditorEncryptedAmountValue;
auto const actualOpt = tx.getAuditorEncryptedAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
EXPECT_TRUE(tx.hasAuditorEncryptedAmount());
}
{
auto const& expected = credentialIDsValue;
auto const actualOpt = tx.getCredentialIDs();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
expectEqualField(expected, *actualOpt, "sfCredentialIDs");
EXPECT_TRUE(tx.hasCredentialIDs());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsConfidentialMPTSendTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTSendFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const destinationValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const senderEncryptedAmountValue = canonical_VL();
auto const destinationEncryptedAmountValue = canonical_VL();
auto const issuerEncryptedAmountValue = canonical_VL();
auto const auditorEncryptedAmountValue = canonical_VL();
auto const zKProofValue = canonical_VL();
auto const amountCommitmentValue = canonical_VL();
auto const balanceCommitmentValue = canonical_VL();
auto const credentialIDsValue = canonical_VECTOR256();
// Build an initial transaction
ConfidentialMPTSendBuilder initialBuilder{
accountValue,
mPTokenIssuanceIDValue,
destinationValue,
senderEncryptedAmountValue,
destinationEncryptedAmountValue,
issuerEncryptedAmountValue,
zKProofValue,
amountCommitmentValue,
balanceCommitmentValue,
sequenceValue,
feeValue
};
initialBuilder.setDestinationTag(destinationTagValue);
initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
initialBuilder.setCredentialIDs(credentialIDsValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
ConfidentialMPTSendBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = rebuiltTx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
{
auto const& expected = destinationValue;
auto const actual = rebuiltTx.getDestination();
expectEqualField(expected, actual, "sfDestination");
}
{
auto const& expected = senderEncryptedAmountValue;
auto const actual = rebuiltTx.getSenderEncryptedAmount();
expectEqualField(expected, actual, "sfSenderEncryptedAmount");
}
{
auto const& expected = destinationEncryptedAmountValue;
auto const actual = rebuiltTx.getDestinationEncryptedAmount();
expectEqualField(expected, actual, "sfDestinationEncryptedAmount");
}
{
auto const& expected = issuerEncryptedAmountValue;
auto const actual = rebuiltTx.getIssuerEncryptedAmount();
expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
}
{
auto const& expected = zKProofValue;
auto const actual = rebuiltTx.getZKProof();
expectEqualField(expected, actual, "sfZKProof");
}
{
auto const& expected = amountCommitmentValue;
auto const actual = rebuiltTx.getAmountCommitment();
expectEqualField(expected, actual, "sfAmountCommitment");
}
{
auto const& expected = balanceCommitmentValue;
auto const actual = rebuiltTx.getBalanceCommitment();
expectEqualField(expected, actual, "sfBalanceCommitment");
}
// Verify optional fields
{
auto const& expected = destinationTagValue;
auto const actualOpt = rebuiltTx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
}
{
auto const& expected = auditorEncryptedAmountValue;
auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
}
{
auto const& expected = credentialIDsValue;
auto const actualOpt = rebuiltTx.getCredentialIDs();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
expectEqualField(expected, *actualOpt, "sfCredentialIDs");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTSendTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTSend{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsConfidentialMPTSendTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(ConfidentialMPTSendBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsConfidentialMPTSendTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTSendNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
auto const mPTokenIssuanceIDValue = canonical_UINT192();
auto const destinationValue = canonical_ACCOUNT();
auto const senderEncryptedAmountValue = canonical_VL();
auto const destinationEncryptedAmountValue = canonical_VL();
auto const issuerEncryptedAmountValue = canonical_VL();
auto const zKProofValue = canonical_VL();
auto const amountCommitmentValue = canonical_VL();
auto const balanceCommitmentValue = canonical_VL();
ConfidentialMPTSendBuilder builder{
accountValue,
mPTokenIssuanceIDValue,
destinationValue,
senderEncryptedAmountValue,
destinationEncryptedAmountValue,
issuerEncryptedAmountValue,
zKProofValue,
amountCommitmentValue,
balanceCommitmentValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasDestinationTag());
EXPECT_FALSE(tx.getDestinationTag().has_value());
EXPECT_FALSE(tx.hasAuditorEncryptedAmount());
EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value());
EXPECT_FALSE(tx.hasCredentialIDs());
EXPECT_FALSE(tx.getCredentialIDs().has_value());
}
}
@@ -35,6 +35,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip)
auto const mPTokenMetadataValue = canonical_VL();
auto const transferFeeValue = canonical_UINT16();
auto const mutableFlagsValue = canonical_UINT32();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
MPTokenIssuanceSetBuilder builder{
accountValue,
@@ -49,6 +51,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip)
builder.setMPTokenMetadata(mPTokenMetadataValue);
builder.setTransferFee(transferFeeValue);
builder.setMutableFlags(mutableFlagsValue);
builder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
auto tx = builder.build(publicKey, secretKey);
@@ -112,6 +116,22 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip)
EXPECT_TRUE(tx.hasMutableFlags());
}
{
auto const& expected = issuerEncryptionKeyValue;
auto const actualOpt = tx.getIssuerEncryptionKey();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptionKey should be present";
expectEqualField(expected, *actualOpt, "sfIssuerEncryptionKey");
EXPECT_TRUE(tx.hasIssuerEncryptionKey());
}
{
auto const& expected = auditorEncryptionKeyValue;
auto const actualOpt = tx.getAuditorEncryptionKey();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptionKey should be present";
expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey");
EXPECT_TRUE(tx.hasAuditorEncryptionKey());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -134,6 +154,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip)
auto const mPTokenMetadataValue = canonical_VL();
auto const transferFeeValue = canonical_UINT16();
auto const mutableFlagsValue = canonical_UINT32();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
// Build an initial transaction
MPTokenIssuanceSetBuilder initialBuilder{
@@ -148,6 +170,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip)
initialBuilder.setMPTokenMetadata(mPTokenMetadataValue);
initialBuilder.setTransferFee(transferFeeValue);
initialBuilder.setMutableFlags(mutableFlagsValue);
initialBuilder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
initialBuilder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -207,6 +231,20 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip)
expectEqualField(expected, *actualOpt, "sfMutableFlags");
}
{
auto const& expected = issuerEncryptionKeyValue;
auto const actualOpt = rebuiltTx.getIssuerEncryptionKey();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptionKey should be present";
expectEqualField(expected, *actualOpt, "sfIssuerEncryptionKey");
}
{
auto const& expected = auditorEncryptionKeyValue;
auto const actualOpt = rebuiltTx.getAuditorEncryptionKey();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptionKey should be present";
expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -274,6 +312,10 @@ TEST(TransactionsMPTokenIssuanceSetTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(tx.getTransferFee().has_value());
EXPECT_FALSE(tx.hasMutableFlags());
EXPECT_FALSE(tx.getMutableFlags().has_value());
EXPECT_FALSE(tx.hasIssuerEncryptionKey());
EXPECT_FALSE(tx.getIssuerEncryptionKey().has_value());
EXPECT_FALSE(tx.hasAuditorEncryptionKey());
EXPECT_FALSE(tx.getAuditorEncryptionKey().has_value());
}
}