mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-08 12:48:14 +00:00
feat: Implement Confidential mpt holder key update (#8266)
This commit is contained in:
@@ -255,6 +255,7 @@ words:
|
||||
- queuable
|
||||
- Raphson
|
||||
- rcflags
|
||||
- reencrypted
|
||||
- replayer
|
||||
- repodata
|
||||
- repomd
|
||||
|
||||
@@ -238,6 +238,12 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
|
||||
TF_FLAG(tfSponsorshipEnd, 0x00010000) \
|
||||
TF_FLAG(tfSponsorshipCreate, 0x00020000) \
|
||||
TF_FLAG(tfSponsorshipReassign, 0x00040000), \
|
||||
MASK_ADJ(0)) \
|
||||
\
|
||||
TRANSACTION(ConfidentialMPTHolderKeyUpdate, \
|
||||
TF_FLAG(tfHolderKeyRotation, 0x00010000) \
|
||||
TF_FLAG(tfHolderKeyRecovery, 0x00020000) \
|
||||
TF_FLAG(tfCancelRecovery, 0x00040000), \
|
||||
MASK_ADJ(0))
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -439,6 +439,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfIssuerKeyMirrorEpoch, SoeOptional},
|
||||
{sfAuditorKeyMirrorEpoch, SoeOptional},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfRecoveryKey, SoeOptional},
|
||||
}))
|
||||
|
||||
/** A ledger object which tracks Oracle
|
||||
|
||||
@@ -330,6 +330,7 @@ TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
|
||||
TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBytecode, VL, 47)
|
||||
TYPED_SFIELD(sfRecoveryKey, VL, 48)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -1147,12 +1147,27 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate,
|
||||
{sfZKProof, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This transaction rotates or recovers a confidential MPT holder's ElGamal encryption key,
|
||||
or cancels a recovery. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, 93, ConfidentialMPTHolderKeyUpdate,
|
||||
({.amendment = featureConfidentialMPTKeyRotation}),
|
||||
({
|
||||
{sfMPTokenIssuanceID, SoeRequired},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfConfidentialBalanceSpending, SoeOptional},
|
||||
{sfConfidentialBalanceInbox, SoeOptional},
|
||||
{sfZKProof, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction posts an unsigned transaction on-ledger as a
|
||||
TransactionProposal, pending multi-signature collection. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
|
||||
#endif
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 93, TransactionProposalCreate,
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
|
||||
({.amendment = featureCosign}),
|
||||
({
|
||||
{sfProposedTransaction, SoeRequired},
|
||||
|
||||
@@ -339,6 +339,30 @@ public:
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfHolderEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfRecoveryKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getRecoveryKey() const
|
||||
{
|
||||
if (hasRecoveryKey())
|
||||
return this->sle_->at(sfRecoveryKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfRecoveryKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasRecoveryKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfRecoveryKey);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -552,6 +576,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfRecoveryKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenBuilder&
|
||||
setRecoveryKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfRecoveryKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed MPToken wrapper.
|
||||
* @param index The ledger entry index.
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
// 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 ConfidentialMPTHolderKeyUpdateBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: ConfidentialMPTHolderKeyUpdate
|
||||
*
|
||||
* Type: ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE (93)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureConfidentialMPTKeyRotation
|
||||
* Privileges: Privilege::NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use ConfidentialMPTHolderKeyUpdateBuilder to construct new transactions.
|
||||
*/
|
||||
class ConfidentialMPTHolderKeyUpdate : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE;
|
||||
|
||||
/**
|
||||
* @brief Construct a ConfidentialMPTHolderKeyUpdate transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit ConfidentialMPTHolderKeyUpdate(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 ConfidentialMPTHolderKeyUpdate");
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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 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->tx_->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->tx_->isFieldPresent(sfConfidentialBalanceSpending);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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->tx_->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->tx_->isFieldPresent(sfConfidentialBalanceInbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 ConfidentialMPTHolderKeyUpdate 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 ConfidentialMPTHolderKeyUpdateBuilder : public TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ConfidentialMPTHolderKeyUpdateBuilder 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.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder(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<ConfidentialMPTHolderKeyUpdateBuilder>(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, account, sequence, fee)
|
||||
{
|
||||
setMPTokenIssuanceID(mPTokenIssuanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a ConfidentialMPTHolderKeyUpdateBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdateBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfMPTokenIssuanceID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
|
||||
{
|
||||
object_[sfMPTokenIssuanceID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfHolderEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setHolderEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfHolderEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialBalanceSpending (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setConfidentialBalanceSpending(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfConfidentialBalanceSpending] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialBalanceInbox (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setConfidentialBalanceInbox(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfConfidentialBalanceInbox] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfZKProof (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfZKProof] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the ConfidentialMPTHolderKeyUpdate wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdate
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return ConfidentialMPTHolderKeyUpdate{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -18,7 +18,7 @@ class TransactionProposalCreateBuilder;
|
||||
/**
|
||||
* @brief Transaction: TransactionProposalCreate
|
||||
*
|
||||
* Type: ttTRANSACTION_PROPOSAL_CREATE (93)
|
||||
* Type: ttTRANSACTION_PROPOSAL_CREATE (95)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureCosign
|
||||
* Privileges: Privilege::NoPriv
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Allows a confidential MPT holder to rotate or recover their ElGamal
|
||||
* encryption key, or to cancel a pending recovery.
|
||||
*
|
||||
* Submitted by the holder. Exactly one of three modes must be selected via
|
||||
* the transaction flags:
|
||||
*
|
||||
* - Rotation (tfHolderKeyRotation): the holder still has their current
|
||||
* ElGamal private key. They provide a new public key along with their
|
||||
* current spending/inbox balances re-encrypted under that new key. The
|
||||
* holder's encryption key and confidential balances are updated
|
||||
* immediately.
|
||||
*
|
||||
* - Recovery (tfHolderKeyRecovery): the holder has lost their current
|
||||
* private key. They provide a new public key but cannot provide
|
||||
* re-encrypted balances. The new key is recorded as a pending
|
||||
* sfRecoveryKey; the confidential balances are left untouched. Completing
|
||||
* the recovery (rewriting the balances) is done separately by the issuer
|
||||
* via ConfidentialMPTRecoverBalance.
|
||||
*
|
||||
* - Cancel (tfCancelRecovery): the holder revokes a pending recovery
|
||||
* authorization created by a prior Recovery-mode transaction. No key,
|
||||
* balances, or proof are carried; authorization is via the holder's
|
||||
* ordinary XRPL signature. sfRecoveryKey is removed and everything else
|
||||
* is left untouched. Fails if no recovery is pending.
|
||||
*
|
||||
* @note Zero-knowledge proof verification for Rotation and Recovery is
|
||||
* deferred to a follow-up once the mpt-crypto constructions are finalized.
|
||||
*/
|
||||
class ConfidentialMPTHolderKeyUpdate : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit ConfidentialMPTHolderKeyUpdate(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static std::uint32_t
|
||||
getFlagsMask(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, SLE::ConstRef before, SLE::ConstRef after) override;
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalizeInvariants(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -41,6 +41,7 @@ constexpr auto kConfidentialMptTxTypes = std::to_array<TxType>({
|
||||
ttCONFIDENTIAL_MPT_MERGE_INBOX,
|
||||
ttCONFIDENTIAL_MPT_CLAWBACK,
|
||||
ttCONFIDENTIAL_MPT_MIRROR_UPDATE,
|
||||
ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE,
|
||||
});
|
||||
|
||||
// Clamp to the cap (== INT64_MAX) before the signed conversion. Invariant
|
||||
@@ -825,10 +826,10 @@ ValidConfidentialMPToken::finalize(
|
||||
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
|
||||
// Reaching here means this confidential MPT transaction left coaDelta
|
||||
// unmodified (e.g. ConfidentialMPTSend, ConfidentialMPTMergeInbox, or
|
||||
// ConfidentialMPTHolderKeyUpdate/ConfidentialMPTMirrorUpdate, none of which touch
|
||||
// sfConfidentialOutstandingAmount), so it must not modify sfOutstandingAmount either.
|
||||
if (checks.outstandingDelta != 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: OutstandingAmount changed "
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#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>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool
|
||||
ConfidentialMPTHolderKeyUpdate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
// Holder key update is only meaningful when confidential transfers are enabled.
|
||||
return ctx.rules.enabled(featureConfidentialTransfer);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
ConfidentialMPTHolderKeyUpdate::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfConfidentialMPTHolderKeyUpdateMask;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
ConfidentialMPTHolderKeyUpdate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
bool const rotation = ctx.tx.isFlag(tfHolderKeyRotation);
|
||||
bool const recovery = ctx.tx.isFlag(tfHolderKeyRecovery);
|
||||
bool const cancel = ctx.tx.isFlag(tfCancelRecovery);
|
||||
|
||||
// Exactly one of the three mode flags must be set.
|
||||
static constexpr auto modeFlags = tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery;
|
||||
if (std::popcount(ctx.tx.getFlags() & modeFlags) != 1)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
// The issuer cannot hold confidential balances.
|
||||
if (ctx.tx[sfAccount] == MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer())
|
||||
return temMALFORMED;
|
||||
|
||||
bool const hasHolderKey = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
|
||||
bool const hasSpending = ctx.tx.isFieldPresent(sfConfidentialBalanceSpending);
|
||||
bool const hasInbox = ctx.tx.isFieldPresent(sfConfidentialBalanceInbox);
|
||||
bool const hasProof = ctx.tx.isFieldPresent(sfZKProof);
|
||||
|
||||
if (cancel)
|
||||
{
|
||||
// Cancel mode only revokes a pending recovery authorization; it
|
||||
// carries no key material, balances, or proof.
|
||||
if (hasHolderKey || hasSpending || hasInbox || hasProof)
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Rotation and Recovery both require a holder key and a proof.
|
||||
if (!hasHolderKey || !hasProof)
|
||||
return temMALFORMED;
|
||||
|
||||
// Rotation mode requires re-encrypted balances; Recovery mode must not
|
||||
// provide them since the holder cannot decrypt the current ones.
|
||||
if (rotation && (!hasSpending || !hasInbox))
|
||||
return temMALFORMED;
|
||||
|
||||
if (recovery && (hasSpending || hasInbox))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasSpending &&
|
||||
ctx.tx[sfConfidentialBalanceSpending].length() != kEcGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasInbox && ctx.tx[sfConfidentialBalanceInbox].length() != kEcGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasSpending && !isValidCiphertext(ctx.tx[sfConfidentialBalanceSpending]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasInbox && !isValidCiphertext(ctx.tx[sfConfidentialBalanceInbox]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
ConfidentialMPTHolderKeyUpdate::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTHolderKeyUpdate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
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(sfConfidentialBalanceInbox))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFlag(tfCancelRecovery))
|
||||
{
|
||||
// Nothing to cancel if no recovery is pending.
|
||||
if (!sleMptoken->isFieldPresent(sfRecoveryKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (ctx.tx[sfHolderEncryptionKey] == (*sleMptoken)[sfHolderEncryptionKey])
|
||||
return tecDUPLICATE;
|
||||
|
||||
// Recovery mode: reject if a recovery is already pending
|
||||
if (ctx.tx.isFlag(tfHolderKeyRecovery) && sleMptoken->isFieldPresent(sfRecoveryKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTHolderKeyUpdate::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
|
||||
if (!sleMptoken)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::ConfidentialMPTHolderKeyUpdate::doApply : preclaim already validated the "
|
||||
"MPToken exists");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (ctx_.tx.isFlag(tfCancelRecovery))
|
||||
{
|
||||
// The holder revokes their pending recovery authorization; the
|
||||
// current key and balances are left untouched.
|
||||
sleMptoken->makeFieldAbsent(sfRecoveryKey);
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
auto const newPubKey = ctx_.tx[sfHolderEncryptionKey];
|
||||
|
||||
if (ctx_.tx.isFlag(tfHolderKeyRotation))
|
||||
{
|
||||
// Replace the key and balances in rotation mode. Rotation proves the
|
||||
// holder still has the old key, so any pending recovery is cancelled.
|
||||
(*sleMptoken)[sfHolderEncryptionKey] = newPubKey;
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending] = ctx_.tx[sfConfidentialBalanceSpending];
|
||||
(*sleMptoken)[sfConfidentialBalanceInbox] = ctx_.tx[sfConfidentialBalanceInbox];
|
||||
sleMptoken->makeFieldAbsent(sfRecoveryKey);
|
||||
incrementConfidentialVersion(*sleMptoken);
|
||||
}
|
||||
else if (ctx_.tx.isFlag(tfHolderKeyRecovery))
|
||||
{
|
||||
// Recovery mode: the holder cannot decrypt their current balances,
|
||||
// so only the pending recovery key is recorded. The balances are
|
||||
// rewritten separately by the issuer via ConfidentialMPTRecoverBalance.
|
||||
(*sleMptoken)[sfRecoveryKey] = newPubKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::ConfidentialMPTHolderKeyUpdate::doApply : invalid mode");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
ConfidentialMPTHolderKeyUpdate::visitInvariantEntry(bool, SLE::ConstRef, SLE::ConstRef)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
ConfidentialMPTHolderKeyUpdate::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/ConfidentialTransfer.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
@@ -20,6 +21,7 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -2454,6 +2456,703 @@ class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testConfidentialMPTHolderKeyUpdatePreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("ConfidentialMPTHolderKeyUpdate preflight");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
|
||||
// Both amendments are required: ConfidentialMPTKeyRotation and ConfidentialTransfer.
|
||||
if (!features[featureConfidentialMPTKeyRotation] || !features[featureConfidentialTransfer])
|
||||
{
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temDISABLED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ConfidentialEnv ct{env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
// The flag contains a value outside the recognized mode bits. This is
|
||||
// rejected by the flags-mask check
|
||||
for (auto const flags : {0x00080000u, 0x00080000u | tfHolderKeyRecovery, 0x00100000u})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = flags,
|
||||
.err = temINVALID_FLAG,
|
||||
});
|
||||
}
|
||||
|
||||
// Exactly one of Rotation, Recovery, and Cancel must be set.
|
||||
for (auto const flags :
|
||||
{0u,
|
||||
tfHolderKeyRotation | tfHolderKeyRecovery,
|
||||
tfHolderKeyRotation | tfCancelRecovery,
|
||||
tfHolderKeyRecovery | tfCancelRecovery,
|
||||
tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = flags,
|
||||
.err = temINVALID_FLAG,
|
||||
});
|
||||
}
|
||||
|
||||
// The issuer cannot rotate or recover a confidential balance it cannot hold.
|
||||
Account const aliceNewKey("aliceNewKey");
|
||||
ct.mpt.generateKeyPair(aliceNewKey);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = alice,
|
||||
.holderPubKey = ct.mpt.getPubKey(aliceNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// HolderEncryptionKey one byte short of the required length.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength - 1),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// HolderEncryptionKey the correct length, but not a well-formed
|
||||
// compressed secp256k1 point.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// HolderEncryptionKey is entirely absent (as opposed to present with
|
||||
// the wrong length or format).
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
std::optional<Buffer> const cipher = getTrivialCiphertext();
|
||||
std::optional<Buffer> const none;
|
||||
|
||||
// Rotation mode requires both the spending and inbox ciphertexts;
|
||||
for (auto const& [spending, inbox] :
|
||||
{std::pair{none, none}, std::pair{cipher, none}, std::pair{none, cipher}})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = spending,
|
||||
.inboxCiphertext = inbox,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
}
|
||||
|
||||
// Recovery and Cancel modes must not include either.
|
||||
for (auto const& [spending, inbox] :
|
||||
{std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = spending,
|
||||
.inboxCiphertext = inbox,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
}
|
||||
|
||||
for (auto const& [spending, inbox] :
|
||||
{std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
|
||||
{
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.spendingCiphertext = spending,
|
||||
.inboxCiphertext = inbox,
|
||||
.flags = tfCancelRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
}
|
||||
|
||||
// Spending ciphertext has the wrong length.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
|
||||
.inboxCiphertext = getTrivialCiphertext(),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temBAD_CIPHERTEXT,
|
||||
});
|
||||
|
||||
// Inbox ciphertext has the wrong length.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = getTrivialCiphertext(),
|
||||
.inboxCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temBAD_CIPHERTEXT,
|
||||
});
|
||||
|
||||
// Spending ciphertext has the correct length, but is not a
|
||||
// well-formed EC ElGamal ciphertext.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = getBadCiphertext(),
|
||||
.inboxCiphertext = getTrivialCiphertext(),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temBAD_CIPHERTEXT,
|
||||
});
|
||||
|
||||
// Inbox ciphertext has the correct length, but is not a well-formed
|
||||
// EC ElGamal ciphertext.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = getTrivialCiphertext(),
|
||||
.inboxCiphertext = getBadCiphertext(),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
.err = temBAD_CIPHERTEXT,
|
||||
});
|
||||
|
||||
// Rotation/Recovery mode requires a ZKProof.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// Cancel mode must not include HolderEncryptionKey.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.flags = tfCancelRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// Cancel mode must not include a ZKProof.
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfCancelRecovery,
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testConfidentialMPTHolderKeyUpdatePreclaim(FeatureBitset features)
|
||||
{
|
||||
testcase("ConfidentialMPTHolderKeyUpdate preclaim");
|
||||
using namespace test::jtx;
|
||||
|
||||
// The issuance does not exist.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
// Destroy the issuance to test issuance not found.
|
||||
mptAlice.destroy();
|
||||
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
.err = tecOBJECT_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
// The issuance has not enabled confidential balances. The holder is
|
||||
// authorized so this reaches the confidential-balance-flag check
|
||||
// rather than failing earlier on a missing MPToken.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
|
||||
mptAlice.authorize({.account = bob});
|
||||
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// carol exists as an account but was never authorized to hold this
|
||||
// issuance, so she has no MPToken for it at all.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const carol("carol");
|
||||
MPTTester mptAlice(env, alice);
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
env.fund(XRP(1000), carol);
|
||||
env.close();
|
||||
|
||||
Account const carolNewKey("carolNewKey");
|
||||
mptAlice.generateKeyPair(carolNewKey);
|
||||
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = carol,
|
||||
.holderPubKey = mptAlice.getPubKey(carolNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = tecOBJECT_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
// The holder has an MPToken but no confidential state yet - never
|
||||
// registered a key or converted anything.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
mptAlice.generateKeyPair(bobNewKey);
|
||||
|
||||
mptAlice.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = mptAlice.getPubKey(bobNewKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// Submitting the holder's own current key as the "new" key is a no-op.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bob),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
}
|
||||
|
||||
// A second Recovery-mode transaction must not silently overwrite an
|
||||
// already-pending RecoveryKey.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
Account const bobRecoveryKey2("bobRecoveryKey2");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey2);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey2),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// bob never submitted a Recovery-mode transaction, so there is no
|
||||
// sfRecoveryKey to cancel.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
// Runs rotation, recovery, and cancel in sequence and expects each to
|
||||
// succeed.
|
||||
auto const allModesSucceed = [&](ConfidentialEnv& ct, Account const& bob) {
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
|
||||
BEAST_EXPECT(reEnc.has_value());
|
||||
if (!reEnc)
|
||||
return;
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = reEnc->first,
|
||||
.inboxCiphertext = reEnc->second,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
});
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
});
|
||||
};
|
||||
|
||||
// Individual (per-holder) lock.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
ct.mpt.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
|
||||
allModesSucceed(ct, bob);
|
||||
}
|
||||
|
||||
// Global (issuance-wide) lock.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
ct.mpt.set({.account = alice, .flags = tfMPTLock});
|
||||
|
||||
allModesSucceed(ct, bob);
|
||||
}
|
||||
|
||||
// A stale mirror epoch does not block rotation, recovery, or cancel:
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
// Rotate the issuer's key without refreshing bob's mirror,
|
||||
// leaving bob's mirror epoch stale relative to the issuance.
|
||||
Account const aliceNewIssuerKey("aliceNewIssuerKey");
|
||||
ct.mpt.generateKeyPair(aliceNewIssuerKey);
|
||||
ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(aliceNewIssuerKey)});
|
||||
BEAST_EXPECT(ct.mpt.checkKeyEpochs(1u, std::nullopt));
|
||||
BEAST_EXPECT(ct.mpt.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
|
||||
|
||||
allModesSucceed(ct, bob);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testConfidentialMPTHolderKeyUpdateDoApply(FeatureBitset features)
|
||||
{
|
||||
testcase("ConfidentialMPTHolderKeyUpdate doApply");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Rotation mode updates the key and re-encrypted balances, bumps the
|
||||
// version, and clears any pending recovery.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
|
||||
BEAST_EXPECT(reEnc.has_value());
|
||||
if (!reEnc)
|
||||
return;
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = reEnc->first,
|
||||
.inboxCiphertext = reEnc->second,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
});
|
||||
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
|
||||
auto const newPubKey = ct.mpt.getPubKey(bobNewKey);
|
||||
BEAST_EXPECT(
|
||||
newPubKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*newPubKey));
|
||||
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
|
||||
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion + 1);
|
||||
|
||||
// The rotated balances must still decrypt to the same amounts,
|
||||
// but now only under the NEW private key.
|
||||
auto const spendingCt =
|
||||
ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
|
||||
auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
|
||||
BEAST_EXPECT(spendingCt.has_value());
|
||||
BEAST_EXPECT(inboxCt.has_value());
|
||||
if (!spendingCt || !inboxCt)
|
||||
return;
|
||||
|
||||
auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
|
||||
auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
|
||||
BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
|
||||
BEAST_EXPECT(inboxAmt && *inboxAmt == 0);
|
||||
}
|
||||
|
||||
// Rotation mode re-encrypts both balances correctly when spending and
|
||||
// inbox are both non-zero and differ from each other.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
ConfidentialEnv ct{
|
||||
env,
|
||||
alice,
|
||||
{{.account = bob, .payAmount = 100, .convertAmount = 40},
|
||||
{.account = carol, .payAmount = 100, .convertAmount = 50}}};
|
||||
|
||||
// carol sends 15 into bob's inbox, which is not merged into
|
||||
// spending, leaving bob with spending=40, inbox=15.
|
||||
ct.mpt.send({.account = carol, .dest = bob, .amt = 15});
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
|
||||
BEAST_EXPECT(reEnc.has_value());
|
||||
if (!reEnc)
|
||||
return;
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = reEnc->first,
|
||||
.inboxCiphertext = reEnc->second,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
});
|
||||
|
||||
auto const spendingCt =
|
||||
ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
|
||||
auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
|
||||
BEAST_EXPECT(spendingCt.has_value());
|
||||
BEAST_EXPECT(inboxCt.has_value());
|
||||
if (!spendingCt || !inboxCt)
|
||||
return;
|
||||
|
||||
auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
|
||||
auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
|
||||
BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
|
||||
BEAST_EXPECT(inboxAmt && *inboxAmt == 15);
|
||||
}
|
||||
|
||||
// Recovery mode only records the pending recovery key; the current
|
||||
// key, balances, and version are untouched.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
|
||||
auto const prevSpending =
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
|
||||
auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
|
||||
auto const prevKey = ct.mpt.getPubKey(bob);
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
|
||||
auto const recoveryKey = ct.mpt.getPubKey(bobRecoveryKey);
|
||||
BEAST_EXPECT(
|
||||
sleMptoken->isFieldPresent(sfRecoveryKey) && recoveryKey &&
|
||||
strHex((*sleMptoken)[sfRecoveryKey]) == strHex(*recoveryKey));
|
||||
|
||||
BEAST_EXPECT(
|
||||
prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
|
||||
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
|
||||
BEAST_EXPECT(
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
|
||||
prevSpending);
|
||||
BEAST_EXPECT(
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
|
||||
}
|
||||
|
||||
// Cancel mode clears the pending recovery key only; the current key,
|
||||
// balances, and version are untouched.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
|
||||
auto const prevSpending =
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
|
||||
auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
|
||||
auto const prevKey = ct.mpt.getPubKey(bob);
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.flags = tfCancelRecovery,
|
||||
});
|
||||
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
|
||||
BEAST_EXPECT(
|
||||
prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
|
||||
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
|
||||
BEAST_EXPECT(
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
|
||||
prevSpending);
|
||||
BEAST_EXPECT(
|
||||
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
|
||||
}
|
||||
|
||||
// Rotation mode clears a pending recovery key.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
ConfidentialEnv ct{
|
||||
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
|
||||
|
||||
Account const bobRecoveryKey("bobRecoveryKey");
|
||||
ct.mpt.generateKeyPair(bobRecoveryKey);
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRecovery,
|
||||
});
|
||||
|
||||
{
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
BEAST_EXPECT(sleMptoken->isFieldPresent(sfRecoveryKey));
|
||||
}
|
||||
|
||||
Account const bobNewKey("bobNewKey");
|
||||
ct.mpt.generateKeyPair(bobNewKey);
|
||||
|
||||
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
|
||||
BEAST_EXPECT(reEnc.has_value());
|
||||
if (!reEnc)
|
||||
return;
|
||||
|
||||
ct.mpt.holderKeyUpdate({
|
||||
.account = bob,
|
||||
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
|
||||
.spendingCiphertext = reEnc->first,
|
||||
.inboxCiphertext = reEnc->second,
|
||||
.proof = gMakeZeroBuffer(1),
|
||||
.flags = tfHolderKeyRotation,
|
||||
});
|
||||
|
||||
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
|
||||
if (!BEAST_EXPECT(sleMptoken))
|
||||
return;
|
||||
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
|
||||
@@ -2490,6 +3189,12 @@ public:
|
||||
testConfidentialMPTMirrorUpdateDoApply(all);
|
||||
testConfidentialMPTMirrorUpdateMultipleRotationsIssuerMode(all);
|
||||
testConfidentialMPTMirrorUpdateMultipleRotationsHolderMode(all);
|
||||
|
||||
testConfidentialMPTHolderKeyUpdatePreflight(all);
|
||||
testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialMPTKeyRotation);
|
||||
testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialTransfer);
|
||||
testConfidentialMPTHolderKeyUpdatePreclaim(all);
|
||||
testConfidentialMPTHolderKeyUpdateDoApply(all);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -188,6 +189,17 @@ protected:
|
||||
// Proof layout: [compact_sigma | bulletproof]
|
||||
static constexpr size_t kBulletproofOffset = kEcSendProofLength - kEcDoubleBulletproofLength;
|
||||
|
||||
// Decrypts holder's current spending/inbox balances under currentKey and
|
||||
// re-encrypts them under newKey, returning {spendingCiphertext,
|
||||
// inboxCiphertext}. Returns std::nullopt if either balance is missing or
|
||||
// fails to decrypt.
|
||||
static std::optional<std::pair<Buffer, Buffer>>
|
||||
reencryptHolderBalances(
|
||||
test::jtx::MPTTester& mpt,
|
||||
test::jtx::Account const& holder,
|
||||
test::jtx::Account const& currentKey,
|
||||
test::jtx::Account const& newKey);
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -292,6 +293,30 @@ ConfidentialTransferTestBase::ConfidentialSendSetup::sendArgs(
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<std::pair<Buffer, Buffer>>
|
||||
ConfidentialTransferTestBase::reencryptHolderBalances(
|
||||
test::jtx::MPTTester& mpt,
|
||||
test::jtx::Account const& holder,
|
||||
test::jtx::Account const& currentKey,
|
||||
test::jtx::Account const& newKey)
|
||||
{
|
||||
auto const spendingCt =
|
||||
mpt.getEncryptedBalance(holder, test::jtx::MPTTester::holderEncryptedSpending);
|
||||
auto const inboxCt =
|
||||
mpt.getEncryptedBalance(holder, test::jtx::MPTTester::holderEncryptedInbox);
|
||||
if (!spendingCt || !inboxCt)
|
||||
return std::nullopt;
|
||||
|
||||
auto const spendingAmt = mpt.decryptAmount(currentKey, *spendingCt);
|
||||
auto const inboxAmt = mpt.decryptAmount(currentKey, *inboxCt);
|
||||
if (!spendingAmt || !inboxAmt)
|
||||
return std::nullopt;
|
||||
|
||||
return std::pair{
|
||||
mpt.encryptAmount(newKey, *spendingAmt, generateBlindingFactor()),
|
||||
mpt.encryptAmount(newKey, *inboxAmt, generateBlindingFactor())};
|
||||
}
|
||||
|
||||
Buffer const&
|
||||
ConfidentialTransferTestBase::getBadCiphertext()
|
||||
{
|
||||
|
||||
@@ -2291,4 +2291,25 @@ MPTTester::mirrorUpdate(MPTMirrorUpdate const& arg, std::source_location const&
|
||||
submit(arg, {jv, loc});
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::holderKeyUpdate(MPTHolderKeyUpdate const& arg, std::source_location const& loc)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::ConfidentialMPTHolderKeyUpdate;
|
||||
|
||||
setAccountField(jv, arg.account);
|
||||
setIssuanceIdField(jv, arg.id);
|
||||
|
||||
if (arg.holderPubKey)
|
||||
jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey);
|
||||
if (arg.spendingCiphertext)
|
||||
jv[sfConfidentialBalanceSpending.jsonName] = strHex(*arg.spendingCiphertext);
|
||||
if (arg.inboxCiphertext)
|
||||
jv[sfConfidentialBalanceInbox.jsonName] = strHex(*arg.inboxCiphertext);
|
||||
if (arg.proof)
|
||||
jv[sfZKProof.jsonName] = strHex(*arg.proof);
|
||||
|
||||
submit(arg, {jv, loc});
|
||||
}
|
||||
|
||||
} // namespace xrpl::test::jtx
|
||||
|
||||
@@ -78,7 +78,8 @@ fillFee(json::Value& jv, ReadView const& view)
|
||||
auto const txType = jv[jss::TransactionType].asString();
|
||||
if (txType == jss::ConfidentialMPTConvert || txType == jss::ConfidentialMPTConvertBack ||
|
||||
txType == jss::ConfidentialMPTSend || txType == jss::ConfidentialMPTMergeInbox ||
|
||||
txType == jss::ConfidentialMPTClawback || txType == jss::ConfidentialMPTMirrorUpdate)
|
||||
txType == jss::ConfidentialMPTClawback || txType == jss::ConfidentialMPTMirrorUpdate ||
|
||||
txType == jss::ConfidentialMPTHolderKeyUpdate)
|
||||
{
|
||||
jv[jss::Fee] = to_string(base * (kConfidentialFeeMultiplier + 1));
|
||||
}
|
||||
|
||||
@@ -408,6 +408,24 @@ struct MPTMirrorUpdate
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Arguments for building a ConfidentialMPTHolderKeyUpdate test transaction.
|
||||
*/
|
||||
struct MPTHolderKeyUpdate
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
std::optional<MPTID> id = std::nullopt;
|
||||
std::optional<Buffer> holderPubKey = std::nullopt;
|
||||
std::optional<Buffer> spendingCiphertext = std::nullopt;
|
||||
std::optional<Buffer> inboxCiphertext = std::nullopt;
|
||||
std::optional<Buffer> proof = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<XRPAmount> fee = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stores the parameters that are exclusively used to generate a
|
||||
* Pedersen linkage proof.
|
||||
@@ -659,6 +677,11 @@ public:
|
||||
MPTMirrorUpdate const& arg = MPTMirrorUpdate{},
|
||||
std::source_location const& loc = std::source_location::current());
|
||||
|
||||
void
|
||||
holderKeyUpdate(
|
||||
MPTHolderKeyUpdate const& arg = MPTHolderKeyUpdate{},
|
||||
std::source_location const& loc = std::source_location::current());
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkDomainID(std::optional<UInt256> expected) const;
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
auto const recoveryKeyValue = canonical_VL();
|
||||
|
||||
MPTokenBuilder builder{
|
||||
accountValue,
|
||||
@@ -54,6 +55,7 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
builder.setIssuerKeyMirrorEpoch(issuerKeyMirrorEpochValue);
|
||||
builder.setAuditorKeyMirrorEpoch(auditorKeyMirrorEpochValue);
|
||||
builder.setHolderEncryptionKey(holderEncryptionKeyValue);
|
||||
builder.setRecoveryKey(recoveryKeyValue);
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
builder.setFlags(0x1u);
|
||||
@@ -174,6 +176,14 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasHolderEncryptionKey());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = recoveryKeyValue;
|
||||
auto const actualOpt = entry.getRecoveryKey();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfRecoveryKey");
|
||||
EXPECT_TRUE(entry.hasRecoveryKey());
|
||||
}
|
||||
|
||||
EXPECT_TRUE(entry.hasLedgerIndex());
|
||||
auto const ledgerIndex = entry.getLedgerIndex();
|
||||
ASSERT_TRUE(ledgerIndex.has_value());
|
||||
@@ -202,6 +212,7 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
auto const recoveryKeyValue = canonical_VL();
|
||||
|
||||
auto sle = std::make_shared<SLE>(MPToken::entryType, index);
|
||||
|
||||
@@ -220,6 +231,7 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfIssuerKeyMirrorEpoch) = issuerKeyMirrorEpochValue;
|
||||
sle->at(sfAuditorKeyMirrorEpoch) = auditorKeyMirrorEpochValue;
|
||||
sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue;
|
||||
sle->at(sfRecoveryKey) = recoveryKeyValue;
|
||||
|
||||
MPTokenBuilder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
@@ -410,6 +422,19 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfHolderEncryptionKey");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = recoveryKeyValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getRecoveryKey();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getRecoveryKey();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfRecoveryKey");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfRecoveryKey");
|
||||
}
|
||||
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
@@ -492,5 +517,7 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getAuditorKeyMirrorEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasHolderEncryptionKey());
|
||||
EXPECT_FALSE(entry.getHolderEncryptionKey().has_value());
|
||||
EXPECT_FALSE(entry.hasRecoveryKey());
|
||||
EXPECT_FALSE(entry.getRecoveryKey().has_value());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
// Auto-generated unit tests for transaction ConfidentialMPTHolderKeyUpdate
|
||||
|
||||
|
||||
#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/ConfidentialMPTHolderKeyUpdate.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(TransactionsConfidentialMPTHolderKeyUpdateTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTHolderKeyUpdate"));
|
||||
|
||||
// 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 holderEncryptionKeyValue = canonical_VL();
|
||||
auto const confidentialBalanceSpendingValue = canonical_VL();
|
||||
auto const confidentialBalanceInboxValue = canonical_VL();
|
||||
auto const zKProofValue = canonical_VL();
|
||||
|
||||
ConfidentialMPTHolderKeyUpdateBuilder builder{
|
||||
accountValue,
|
||||
mPTokenIssuanceIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
builder.setHolderEncryptionKey(holderEncryptionKeyValue);
|
||||
builder.setConfidentialBalanceSpending(confidentialBalanceSpendingValue);
|
||||
builder.setConfidentialBalanceInbox(confidentialBalanceInboxValue);
|
||||
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");
|
||||
}
|
||||
|
||||
// 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 = confidentialBalanceSpendingValue;
|
||||
auto const actualOpt = tx.getConfidentialBalanceSpending();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfConfidentialBalanceSpending should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceSpending");
|
||||
EXPECT_TRUE(tx.hasConfidentialBalanceSpending());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialBalanceInboxValue;
|
||||
auto const actualOpt = tx.getConfidentialBalanceInbox();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfConfidentialBalanceInbox should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceInbox");
|
||||
EXPECT_TRUE(tx.hasConfidentialBalanceInbox());
|
||||
}
|
||||
|
||||
{
|
||||
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(TransactionsConfidentialMPTHolderKeyUpdateTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTHolderKeyUpdateFromTx"));
|
||||
|
||||
// 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 holderEncryptionKeyValue = canonical_VL();
|
||||
auto const confidentialBalanceSpendingValue = canonical_VL();
|
||||
auto const confidentialBalanceInboxValue = canonical_VL();
|
||||
auto const zKProofValue = canonical_VL();
|
||||
|
||||
// Build an initial transaction
|
||||
ConfidentialMPTHolderKeyUpdateBuilder initialBuilder{
|
||||
accountValue,
|
||||
mPTokenIssuanceIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
initialBuilder.setHolderEncryptionKey(holderEncryptionKeyValue);
|
||||
initialBuilder.setConfidentialBalanceSpending(confidentialBalanceSpendingValue);
|
||||
initialBuilder.setConfidentialBalanceInbox(confidentialBalanceInboxValue);
|
||||
initialBuilder.setZKProof(zKProofValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
ConfidentialMPTHolderKeyUpdateBuilder 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
|
||||
{
|
||||
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 = confidentialBalanceSpendingValue;
|
||||
auto const actualOpt = rebuiltTx.getConfidentialBalanceSpending();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfConfidentialBalanceSpending should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceSpending");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialBalanceInboxValue;
|
||||
auto const actualOpt = rebuiltTx.getConfidentialBalanceInbox();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfConfidentialBalanceInbox should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfConfidentialBalanceInbox");
|
||||
}
|
||||
|
||||
{
|
||||
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(TransactionsConfidentialMPTHolderKeyUpdateTests, 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(ConfidentialMPTHolderKeyUpdate{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsConfidentialMPTHolderKeyUpdateTests, 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(ConfidentialMPTHolderKeyUpdateBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(TransactionsConfidentialMPTHolderKeyUpdateTests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTHolderKeyUpdateNullopt"));
|
||||
|
||||
// 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();
|
||||
|
||||
ConfidentialMPTHolderKeyUpdateBuilder builder{
|
||||
accountValue,
|
||||
mPTokenIssuanceIDValue,
|
||||
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.hasConfidentialBalanceSpending());
|
||||
EXPECT_FALSE(tx.getConfidentialBalanceSpending().has_value());
|
||||
EXPECT_FALSE(tx.hasConfidentialBalanceInbox());
|
||||
EXPECT_FALSE(tx.getConfidentialBalanceInbox().has_value());
|
||||
EXPECT_FALSE(tx.hasZKProof());
|
||||
EXPECT_FALSE(tx.getZKProof().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user