diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index 1a77dd15d2..6df4f01397 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -94,7 +94,19 @@ secp256k1_elgamal_subtract( secp256k1_pubkey const* b_c2); /** - * @brief Generates the canonical encrypted zero for a given trust line. + * @brief Generates the canonical encrypted zero for a given MPT token instance. + * + * This ciphertext represents a zero balance for a specific account's holding + * of a token defined by its MPTokenIssuanceID. + * + * @param[in] ctx A pointer to a valid secp256k1 context. + * @param[out] enc_zero_c1 The C1 component of the canonical ciphertext. + * @param[out] enc_zero_c2 The C2 component of the canonical ciphertext. + * @param[in] pubkey The ElGamal public key of the account holder. + * @param[in] account_id A pointer to the 20-byte AccountID. + * @param[in] mpt_issuance_id A pointer to the 24-byte MPTokenIssuanceID. + * + * @return 1 on success, 0 on failure. */ SECP256K1_API int generate_canonical_encrypted_zero( @@ -102,9 +114,9 @@ generate_canonical_encrypted_zero( secp256k1_pubkey* enc_zero_c1, secp256k1_pubkey* enc_zero_c2, secp256k1_pubkey const* pubkey, - char const* acct, - char const* issuer, - char const* curr); + unsigned char const* account_id, // 20 bytes + unsigned char const* mpt_issuance_id // 24 bytes +); // breaks a 66-byte encrypted amount into two 33-byte components // then parses each 33-byte component into 64-byte secp256k1_pubkey format @@ -133,6 +145,11 @@ proveEquality( Buffer encryptAmount(uint64_t amt, Slice const& pubKeySlice); +Buffer +encryptCanonicalZeroAmount( + Slice const& pubKeySlice, + AccountID const& account, + MPTID const& mptId); } // namespace ripple #endif diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 490501788b..23f2473f57 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -962,6 +962,34 @@ TRANSACTION(ttCONFIDENTIAL_CONVERT, 72, ConfidentialConvert, {sfZKProof, soeREQUIRED}, })) +/** This transaction type merges MPT inbox. */ +#if TRANSACTION_INCLUDE +#include +#endif +TRANSACTION(ttCONFIDENTIAL_MERGE_INBOX, 73, ConfidentialMergeInbox, + Delegation::delegatable, + featureConfidentialTransfer, + noPriv, + ({ + {sfMPTokenIssuanceID, soeREQUIRED}, +})) + +/** This transaction type converts back into public MPT balance. */ +#if TRANSACTION_INCLUDE +#include +#endif +TRANSACTION(ttCONFIDENTIAL_CONVERT_BACK, 74, ConfidentialConvertBack, + Delegation::delegatable, + featureConfidentialTransfer, + noPriv, + ({ + {sfMPTokenIssuanceID, soeREQUIRED}, + {sfMPTAmount, soeREQUIRED}, + {sfHolderEncryptedAmount, soeREQUIRED}, + {sfIssuerEncryptedAmount, 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 diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index 3e1e542472..fdcf79351e 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -122,7 +122,6 @@ secp256k1_elgamal_decrypt( secp256k1_pubkey const* c2, unsigned char const* privkey) { - /* C90-compliant variable declarations */ secp256k1_pubkey S, M, G_point, current_M, next_M; secp256k1_pubkey const* points_to_add[2]; unsigned char c2_bytes[33], s_bytes[33], m_bytes[33], current_m_bytes[33]; @@ -266,29 +265,60 @@ secp256k1_elgamal_subtract( return 1; // Success } +// Helper function to concatenate data for hashing +static void +build_hash_input( + unsigned char* output_buffer, + size_t buffer_size, + unsigned char const* account_id, // 20 bytes + unsigned char const* mpt_issuance_id // 24 bytes +) +{ + char const* domain_separator = "EncZero"; + size_t domain_len = strlen(domain_separator); + size_t offset = 0; + + // Ensure buffer is large enough (should be checked by caller if necessary) + // Size = strlen("EncZero") + 20 + 24 = 7 + 20 + 24 = 51 bytes + + memcpy(output_buffer + offset, domain_separator, domain_len); + offset += domain_len; + + memcpy(output_buffer + offset, account_id, 20); + offset += 20; + + memcpy(output_buffer + offset, mpt_issuance_id, 24); + // offset += 24; // Final size is offset + 24 +} + // The canonical encrypted zero + int generate_canonical_encrypted_zero( secp256k1_context const* ctx, secp256k1_pubkey* enc_zero_c1, secp256k1_pubkey* enc_zero_c2, secp256k1_pubkey const* pubkey, - char const* acct, - char const* issuer, - char const* curr) + unsigned char const* account_id, // 20 bytes + unsigned char const* mpt_issuance_id // 24 bytes +) { unsigned char deterministic_scalar[32]; - char input_str[256]; + unsigned char hash_input[51]; // Size calculated above - /* 1. Create the input string for hashing */ - snprintf(input_str, sizeof(input_str), "EncZero%s%s%s", acct, issuer, curr); + /* 1. Create the input buffer for hashing */ + build_hash_input( + hash_input, sizeof(hash_input), account_id, mpt_issuance_id); - /* 2. Hash the string to create the deterministic scalar 'r' */ + /* 2. Hash the buffer to create the deterministic scalar 'r' */ do { - SHA256( - (unsigned char*)input_str, strlen(input_str), deterministic_scalar); + // Hash the concatenated bytes + SHA256(hash_input, sizeof(hash_input), deterministic_scalar); + /* Note: If the hash output could be invalid (0 or >= n), + * you might need to add a nonce/counter to hash_input + * and re-hash in a loop until a valid scalar is produced. */ } while (secp256k1_ec_seckey_verify(ctx, deterministic_scalar) != 1); /* 3. Encrypt the amount 0 using the deterministic scalar */ @@ -442,4 +472,42 @@ encryptAmount(uint64_t amt, Slice const& pubKeySlice) return buf; } +Buffer +encryptCanonicalZeroAmount( + Slice const& pubKeySlice, + AccountID const& account, + MPTID const& mptId) +{ + Buffer buf(ecGamalEncryptedTotalLength); + + // Allocate ciphertext placeholders + secp256k1_pubkey c1, c2; + + // Prepare a random blinding factor + unsigned char blinding_factor[32]; + if (RAND_bytes(blinding_factor, 32) != 1) + Throw("Failed to generate random number"); + + secp256k1_pubkey pubKey; + + std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength); + + // Encrypt the amount + if (!generate_canonical_encrypted_zero( + secp256k1Context(), + &c1, + &c2, + &pubKey, + account.data(), + mptId.data())) + Throw("Failed to encrypt amount"); + + // Serialize the ciphertext pair into the buffer + if (!serializeEcPair(c1, c2, buf)) + Throw( + "Failed to serialize into 66 byte compressed format"); + + return buf; +} + } // namespace ripple diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index d4b5de2553..b61c8b0b29 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -413,6 +413,50 @@ class ConfidentialTransfer_test : public beast::unit_test::suite // todo: test well formed proof } + void + testMergeInbox(FeatureBitset features) + { + testcase("Merge inbox"); + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanLock}); + + mptAlice.authorize({.account = bob}); + env.close(); + mptAlice.pay(alice, bob, 100); + env.close(); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 40, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(bob), + }); + + env.close(); + mptAlice.printMPT(bob); + + mptAlice.mergeInbox({ + .account = bob, + }); + + env.close(); + mptAlice.printMPT(bob); + } + void testWithFeats(FeatureBitset features) { @@ -420,6 +464,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite testConvertPreflight(features); testConvertPreclaim(features); + testMergeInbox(features); testSetPreflight(features); } diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index fc83da173a..0478b07cc4 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -787,6 +787,43 @@ MPTTester::getDecryptedBalance( : 0; }; +void +MPTTester::mergeInbox(MPTMergeInbox const& arg) +{ + Json::Value jv; + if (arg.account) + jv[sfAccount] = arg.account->human(); + else + Throw("Account not specified"); + if (arg.id) + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + jv[sfTransactionType] = jss::ConfidentialMergeInbox; + uint64_t preInboxBalance = + getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); + uint64_t prevSpendingBalance = + getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + + if (submit(arg, jv) == tesSUCCESS) + { + uint64_t postInboxBalance = + getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); + uint64_t postSpendingBalance = + getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + + env_.require(requireAny([&]() -> bool { + return postSpendingBalance == + preInboxBalance + prevSpendingBalance && + postInboxBalance == 0; + })); + } +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 273d02a9ed..c1558c5dc6 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -166,6 +166,16 @@ struct MPTConvert std::optional err = std::nullopt; }; +struct MPTMergeInbox +{ + std::optional account = std::nullopt; + std::optional id = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional err = std::nullopt; +}; + class MPTTester { Env& env_; @@ -200,6 +210,9 @@ public: void convert(MPTConvert const& arg = MPTConvert{}); + void + mergeInbox(MPTMergeInbox const& arg = MPTMergeInbox{}); + [[nodiscard]] bool checkDomainID(std::optional expected) const; diff --git a/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp new file mode 100644 index 0000000000..7331c46d78 --- /dev/null +++ b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp @@ -0,0 +1,174 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +ConfidentialConvertBack::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureConfidentialTransfer)) + return temDISABLED; + + // issuer cannot convert + if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) + return temMALFORMED; + + if (ctx.tx[sfHolderEncryptedAmount].length() != + ecGamalEncryptedTotalLength || + ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength) + return temMALFORMED; + + if (ctx.tx[sfMPTAmount] == 0) + return temMALFORMED; + + // todo: update with correct size of proof since it might also contain range + // proof + if (ctx.tx[sfZKProof].length() != ecEqualityProofLength) + return temMALFORMED; + + return tesSUCCESS; +} + +TER +ConfidentialConvertBack::preclaim(PreclaimContext const& ctx) +{ + // ensure that issuance exists + auto const sleIssuance = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (sleIssuance->isFlag(lsfMPTNoConfidentialTransfer)) + return tecNO_PERMISSION; + + auto const sleMptoken = ctx.view.read( + keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount])); + if (!sleMptoken) + return tecOBJECT_NOT_FOUND; + + if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) || + !sleMptoken->isFieldPresent(sfHolderElGamalPublicKey)) + { + return tecINSUFFICIENT_FUNDS; + } + + // 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->isFieldPresent(sfConfidentialOutstandingAmount) || + (*sleIssuance)[sfConfidentialOutstandingAmount] < ctx.tx[sfMPTAmount]) + { + return tecINSUFFICIENT_FUNDS; + } + + // todo: need addtional parsing, the proof should contain multiple proofs + auto checkEqualityProof = [&](auto const& encryptedAmount, + auto const& pubKey) -> TER { + return proveEquality( + ctx.tx[sfZKProof], + encryptedAmount, + pubKey, + ctx.tx[sfMPTAmount], + ctx.tx.getTransactionID(), + (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0)); + }; + + if (!isTesSuccess(checkEqualityProof( + ctx.tx[sfHolderEncryptedAmount], + (*sleMptoken)[sfHolderElGamalPublicKey])) || + !isTesSuccess(checkEqualityProof( + ctx.tx[sfIssuerEncryptedAmount], + (*sleIssuance)[sfIssuerElGamalPublicKey]))) + { + return tecBAD_PROOF; + } + + // todo: also check range proof that + // sfHolderEncryptedAmount <= sfConfidentialBalanceSpending AND + // sfIssuerEncryptedAmount <= sfIssuerEncryptedBalance + + return tesSUCCESS; +} + +TER +ConfidentialConvertBack::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + + auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_)); + if (!sleMptoken) + return tecINTERNAL; + + auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + return tecINTERNAL; + + auto const amtToConvertBack = ctx_.tx[sfMPTAmount]; + auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0); + + (*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack; + (*sleIssuance)[sfConfidentialOutstandingAmount] = + (*sleIssuance)[sfConfidentialOutstandingAmount] - amtToConvertBack; + + // it's fine if it reaches max uint32, it just resets to 0 + (*sleMptoken)[sfConfidentialBalanceVersion] = + (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0u) + 1u; + + // todo: support homomophic sub + // // homomorphically subtract holder's encrypted balance + // { + // Buffer res(ecGamalEncryptedTotalLength); + // if (TER const ter = homomorphicSub( + // (*sleMptoken)[sfConfidentialBalanceSpending], + // ctx_.tx[sfHolderEncryptedAmount], + // res); + // isTesSuccess(ter)) + // return tecINTERNAL; + + // (*sleMptoken)[sfConfidentialBalanceSpending] = res; + // } + + // // homomorphically subtract issuer's encrypted balance + // { + // Buffer res(ecGamalEncryptedTotalLength); + // if (TER const ter = homomorphicSub( + // (*sleMptoken)[sfIssuerEncryptedBalance], + // ctx_.tx[sfIssuerEncryptedAmount], + // res); + // isTesSuccess(ter)) + // return tecINTERNAL; + + // (*sleMptoken)[sfIssuerEncryptedBalance] = res; + // } + + view().update(sleIssuance); + view().update(sleMptoken); + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/ConfidentialConvertBack.h b/src/xrpld/app/tx/detail/ConfidentialConvertBack.h new file mode 100644 index 0000000000..3f90c1eb86 --- /dev/null +++ b/src/xrpld/app/tx/detail/ConfidentialConvertBack.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_CONFIDENTIALCONVERTBACK_H_INCLUDED +#define RIPPLE_TX_CONFIDENTIALCONVERTBACK_H_INCLUDED + +#include + +namespace ripple { + +class ConfidentialConvertBack : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit ConfidentialConvertBack(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/ConfidentialMergeInbox.cpp b/src/xrpld/app/tx/detail/ConfidentialMergeInbox.cpp new file mode 100644 index 0000000000..0d834c5c33 --- /dev/null +++ b/src/xrpld/app/tx/detail/ConfidentialMergeInbox.cpp @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +ConfidentialMergeInbox::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureConfidentialTransfer)) + return temDISABLED; + + // issuer cannot merge + if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) + return temMALFORMED; + + return tesSUCCESS; +} + +TER +ConfidentialMergeInbox::preclaim(PreclaimContext const& ctx) +{ + auto const sleMptoken = ctx.view.read( + keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount])); + if (!sleMptoken) + return tecOBJECT_NOT_FOUND; + + if (!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) || + !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending)) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + +TER +ConfidentialMergeInbox::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_)); + if (!sleMptoken) + return tecINTERNAL; + + // homomorphically add holder's encrypted balance + Buffer sum(ecGamalEncryptedTotalLength); + if (TER const ter = homomorphicAdd( + (*sleMptoken)[sfConfidentialBalanceSpending], + (*sleMptoken)[sfConfidentialBalanceInbox], + sum); + !isTesSuccess(ter)) + return tecINTERNAL; + + (*sleMptoken)[sfConfidentialBalanceSpending] = sum; + + Buffer zeroEncyption; + zeroEncyption = encryptCanonicalZeroAmount( + (*sleMptoken)[sfHolderElGamalPublicKey], account_, mptIssuanceID); + (*sleMptoken)[sfConfidentialBalanceInbox] = zeroEncyption; + + // it's fine if it reaches max uint32, it just resets to 0 + (*sleMptoken)[sfConfidentialBalanceVersion] = + (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0u) + 1u; + + view().update(sleMptoken); + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/ConfidentialMergeInbox.h b/src/xrpld/app/tx/detail/ConfidentialMergeInbox.h new file mode 100644 index 0000000000..7f18a19cba --- /dev/null +++ b/src/xrpld/app/tx/detail/ConfidentialMergeInbox.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_CONFIDENTIALMERGEINBOX_H_INCLUDED +#define RIPPLE_TX_CONFIDENTIALMERGEINBOX_H_INCLUDED + +#include + +namespace ripple { + +class ConfidentialMergeInbox : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit ConfidentialMergeInbox(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif