mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
support ConfidentialMPTMirrorUpdate
This commit is contained in:
@@ -429,6 +429,8 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfConfidentialBalanceVersion, SoeDefault},
|
||||
{sfIssuerEncryptedBalance, SoeOptional},
|
||||
{sfAuditorEncryptedBalance, SoeOptional},
|
||||
{sfIssuerKeyMirrorEpoch, SoeOptional},
|
||||
{sfAuditorKeyMirrorEpoch, SoeOptional},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
}))
|
||||
|
||||
|
||||
@@ -322,6 +322,7 @@ TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43)
|
||||
TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
|
||||
TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfPreviousIssuerEncryptionKey, VL, 47)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -1131,6 +1131,22 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet,
|
||||
{sfRemainingOwnerCountDelta, SoeOptional},
|
||||
}))
|
||||
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate,
|
||||
Delegation::NotDelegable,
|
||||
featureConfidentialMPTKeyRotation,
|
||||
NoPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, SoeRequired},
|
||||
{sfHolder, SoeOptional},
|
||||
{sfIssuerEncryptedAmount, SoeOptional},
|
||||
{sfAuditorEncryptedAmount, SoeOptional},
|
||||
{sfPreviousIssuerEncryptionKey, SoeOptional},
|
||||
{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
|
||||
|
||||
@@ -268,6 +268,54 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorEncryptedBalance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfIssuerKeyMirrorEpoch (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getIssuerKeyMirrorEpoch() const
|
||||
{
|
||||
if (hasIssuerKeyMirrorEpoch())
|
||||
return this->sle_->at(sfIssuerKeyMirrorEpoch);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfIssuerKeyMirrorEpoch is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasIssuerKeyMirrorEpoch() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfIssuerKeyMirrorEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAuditorKeyMirrorEpoch (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getAuditorKeyMirrorEpoch() const
|
||||
{
|
||||
if (hasAuditorKeyMirrorEpoch())
|
||||
return this->sle_->at(sfAuditorKeyMirrorEpoch);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAuditorKeyMirrorEpoch is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAuditorKeyMirrorEpoch() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyMirrorEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfHolderEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -471,6 +519,28 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfIssuerKeyMirrorEpoch (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenBuilder&
|
||||
setIssuerKeyMirrorEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfIssuerKeyMirrorEpoch] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAuditorKeyMirrorEpoch (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenBuilder&
|
||||
setAuditorKeyMirrorEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAuditorKeyMirrorEpoch] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfHolderEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
// 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 ConfidentialMPTMirrorUpdateBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: ConfidentialMPTMirrorUpdate
|
||||
*
|
||||
* Type: ttCONFIDENTIAL_MPT_MIRROR_UPDATE (92)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureConfidentialMPTKeyRotation
|
||||
* Privileges: NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use ConfidentialMPTMirrorUpdateBuilder to construct new transactions.
|
||||
*/
|
||||
class ConfidentialMPTMirrorUpdate : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_MIRROR_UPDATE;
|
||||
|
||||
/**
|
||||
* @brief Construct a ConfidentialMPTMirrorUpdate transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit ConfidentialMPTMirrorUpdate(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 ConfidentialMPTMirrorUpdate");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfMPTokenIssuanceID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT192::type::value_type
|
||||
getMPTokenIssuanceID() const
|
||||
{
|
||||
return this->tx_->at(sfMPTokenIssuanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfHolder (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_ACCOUNT::type::value_type>
|
||||
getHolder() const
|
||||
{
|
||||
if (hasHolder())
|
||||
{
|
||||
return this->tx_->at(sfHolder);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfHolder is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasHolder() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfHolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfIssuerEncryptedAmount (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getIssuerEncryptedAmount() const
|
||||
{
|
||||
if (hasIssuerEncryptedAmount())
|
||||
{
|
||||
return this->tx_->at(sfIssuerEncryptedAmount);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfIssuerEncryptedAmount is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasIssuerEncryptedAmount() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfIssuerEncryptedAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAuditorEncryptedAmount (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getAuditorEncryptedAmount() const
|
||||
{
|
||||
if (hasAuditorEncryptedAmount())
|
||||
{
|
||||
return this->tx_->at(sfAuditorEncryptedAmount);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAuditorEncryptedAmount is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAuditorEncryptedAmount() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfAuditorEncryptedAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfPreviousIssuerEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getPreviousIssuerEncryptionKey() const
|
||||
{
|
||||
if (hasPreviousIssuerEncryptionKey())
|
||||
{
|
||||
return this->tx_->at(sfPreviousIssuerEncryptionKey);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfPreviousIssuerEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasPreviousIssuerEncryptionKey() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfPreviousIssuerEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfZKProof (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_VL::type::value_type
|
||||
getZKProof() const
|
||||
{
|
||||
return this->tx_->at(sfZKProof);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for ConfidentialMPTMirrorUpdate 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 ConfidentialMPTMirrorUpdateBuilder : public TransactionBuilderBase<ConfidentialMPTMirrorUpdateBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ConfidentialMPTMirrorUpdateBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
|
||||
* @param zKProof The sfZKProof field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
ConfidentialMPTMirrorUpdateBuilder(SF_ACCOUNT::type::value_type account,
|
||||
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::decay_t<typename SF_VL::type::value_type> const& zKProof, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<ConfidentialMPTMirrorUpdateBuilder>(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, account, sequence, fee)
|
||||
{
|
||||
setMPTokenIssuanceID(mPTokenIssuanceID);
|
||||
setZKProof(zKProof);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a ConfidentialMPTMirrorUpdateBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
ConfidentialMPTMirrorUpdateBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_MIRROR_UPDATE)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ConfidentialMPTMirrorUpdateBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfMPTokenIssuanceID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTMirrorUpdateBuilder&
|
||||
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
|
||||
{
|
||||
object_[sfMPTokenIssuanceID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfHolder (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTMirrorUpdateBuilder&
|
||||
setHolder(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
|
||||
{
|
||||
object_[sfHolder] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfIssuerEncryptedAmount (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTMirrorUpdateBuilder&
|
||||
setIssuerEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfIssuerEncryptedAmount] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAuditorEncryptedAmount (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTMirrorUpdateBuilder&
|
||||
setAuditorEncryptedAmount(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAuditorEncryptedAmount] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfPreviousIssuerEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTMirrorUpdateBuilder&
|
||||
setPreviousIssuerEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfPreviousIssuerEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfZKProof (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTMirrorUpdateBuilder&
|
||||
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfZKProof] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the ConfidentialMPTMirrorUpdate wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
ConfidentialMPTMirrorUpdate
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return ConfidentialMPTMirrorUpdate{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
101
include/xrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.h
Normal file
101
include/xrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#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 <memory>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Updates the encrypted mirror balances of a Confidential MPToken.
|
||||
*
|
||||
* @details
|
||||
* This transaction updates a single holder's mirrored confidential balances
|
||||
* (`sfIssuerEncryptedBalance` and/or `sfAuditorEncryptedBalance`) with the latest
|
||||
* ElGamal public keys defined on the `MPTokenIssuance`.
|
||||
*
|
||||
* It supports both issuer and holder self-migration modes, each mode supports multiple flows:
|
||||
* - Issuer mode: Submitted by the issuer.
|
||||
* 1. Issuer Key Rotation Migration: Re-encrypts the
|
||||
* holder's `sfIssuerEncryptedBalance` under the issuer's new ElGamal public key.
|
||||
*
|
||||
* 2. Auditor Key Rotation Migration: Re-encrypts the
|
||||
* holder's `sfAuditorEncryptedBalance` under the auditor's new ElGamal public key.
|
||||
*
|
||||
* 3. Simultaneous Rotation Migration: Updates both the issuer
|
||||
* and auditor encrypted balances in a single transaction to optimize network throughput.
|
||||
*
|
||||
* 4. Auditor Late-Registration Migration: When the issuer ElGamal
|
||||
* public key is already registered on the `MPTokenIssuance` object, the issuer can
|
||||
* register an auditor key at a later time through `MPTokenIssuanceSet`. Then the issuer uses this
|
||||
* flow to set the holder's initial `sfAuditorEncryptedBalance` on `MPToken` object.
|
||||
*
|
||||
* - Holder self-migration mode: Submitted by the holder. This is the recovery
|
||||
* path used when the issuer has permanently lost private key and can no longer perform
|
||||
* active re-encryption. The holder decrypts their own
|
||||
* `sfConfidentialBalanceSpending` with holder's private key to recover the balance and
|
||||
* re-encrypts it under the relevant new ElGamal public key(s).
|
||||
* @note All holder migration flows strictly require the holder's
|
||||
* `sfConfidentialBalanceInbox` to be canonically zero; the holder must run
|
||||
* `ConfidentialMPTMergeInbox` first so the spending balance reflects the
|
||||
* full balance.
|
||||
*
|
||||
* 5. Holder Issuer-Mirror Migration: Re-encrypts the holder's
|
||||
* `sfIssuerEncryptedBalance` under the issuer's new ElGamal public key.
|
||||
*
|
||||
* 6. Holder Auditor-Mirror Migration: Re-encrypts the holder's
|
||||
* `sfAuditorEncryptedBalance` under the auditor's new ElGamal public key, or
|
||||
* sets it for the first time when the auditor key was late-registered. Needed
|
||||
* only because the issuer, having lost sk_I, can no longer perform the auditor
|
||||
* re-encryption or initial registration in issuer mode (flows 2 and 4).
|
||||
*
|
||||
* 7. Simultaneous Holder Self-Migration: Updates both the issuer and auditor
|
||||
* encrypted balances in a single transaction (both keys have rotated).
|
||||
*/
|
||||
class ConfidentialMPTMirrorUpdate : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit ConfidentialMPTMirrorUpdate(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
visitInvariantEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after) override;
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalizeInvariants(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
304
src/libxrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.cpp
Normal file
304
src/libxrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.cpp
Normal file
@@ -0,0 +1,304 @@
|
||||
#include <xrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.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/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool
|
||||
ConfidentialMPTMirrorUpdate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
// Key rotation makes sense only when featureConfidentialTransfer is enabled.
|
||||
return ctx.rules.enabled(featureConfidentialTransfer);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
ConfidentialMPTMirrorUpdate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer();
|
||||
auto const holder = ctx.tx[~sfHolder];
|
||||
bool const hasHolder = holder.has_value();
|
||||
|
||||
// The rotation mode is determined by the presence of the
|
||||
// Holder field: Holder present is issuer mode, Holder absent is
|
||||
// holder self-migration.
|
||||
if (hasHolder)
|
||||
{
|
||||
// Issuer mode: account must be the issuer
|
||||
if (account != issuer)
|
||||
return temMALFORMED;
|
||||
|
||||
if (account == *holder)
|
||||
return temMALFORMED;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Holder self-migration: the submitter is the holder, account must not be the issuer.
|
||||
if (account == issuer)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// At least one ciphertext will be updated.
|
||||
bool const hasIssuerAmount = ctx.tx.isFieldPresent(sfIssuerEncryptedAmount);
|
||||
bool const hasAuditorAmount = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
|
||||
if (!hasIssuerAmount && !hasAuditorAmount)
|
||||
return temMALFORMED;
|
||||
|
||||
// The previous issuer key is required exactly for an issuer-mode
|
||||
// re-encryption of the issuer mirror,either issuer key rotation or
|
||||
// issuer/auditor key simultaneous rotation.
|
||||
bool const hasPreviousIssuerKey = ctx.tx.isFieldPresent(sfPreviousIssuerEncryptionKey);
|
||||
bool const needsPreviousIssuerKey = hasHolder && hasIssuerAmount;
|
||||
if (hasPreviousIssuerKey != needsPreviousIssuerKey)
|
||||
return temMALFORMED;
|
||||
|
||||
// Check the length of the encrypted amounts. Length check is cheaper than format check so put
|
||||
// it before the format check.
|
||||
if (hasIssuerAmount && ctx.tx[sfIssuerEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasAuditorAmount &&
|
||||
ctx.tx[sfAuditorEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
// Check the encrypted amount formats. It is more expensive so put it at the end of preflight.
|
||||
if (hasIssuerAmount && !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasAuditorAmount && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
// If previous issuer key is present, it must be a valid EC point.
|
||||
if (hasPreviousIssuerKey && !isValidCompressedECPoint(ctx.tx[sfPreviousIssuerEncryptionKey]))
|
||||
return temMALFORMED;
|
||||
|
||||
// todo: check zkproof
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
ConfidentialMPTMirrorUpdate::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTMirrorUpdate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// Check if account exists
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
if (!ctx.view.exists(keylet::account(account)))
|
||||
return terNO_ACCOUNT; // LCOV_EXCL_LINE
|
||||
|
||||
// The issuance must exist and have confidential balances enabled with a
|
||||
// registered issuer encryption key; otherwise there is no mirror to update.
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// The issuance must have confidential balances enabled with a registered issuer encryption key.
|
||||
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) ||
|
||||
!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Sanity check: preflight already enforced the issuer holder combination
|
||||
// under different rotation modes.
|
||||
auto const holder = ctx.tx[~sfHolder];
|
||||
bool const hasHolder = holder.has_value();
|
||||
auto const issuer = sleIssuance->getAccountID(sfIssuer);
|
||||
if (hasHolder ? (issuer != account) : (issuer == account))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::ConfidentialMPTMirrorUpdate::preclaim : invalid issuer holder combination");
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// The holder is sfHolder in issuer mode and is sfAccount in holder mode.
|
||||
auto const holderID = hasHolder ? *holder : account;
|
||||
|
||||
// In issuer mode, the holder must exist. In holder mode, the account existence was checked
|
||||
// already.
|
||||
if (hasHolder && !ctx.view.exists(keylet::account(holderID)))
|
||||
return tecNO_TARGET;
|
||||
|
||||
// In either issuer or holder mode, check the existence of the MPToken object.
|
||||
auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, holderID));
|
||||
if (!sleMptoken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// The holder must already hold an issuer confidential balance.
|
||||
if (!sleMptoken->isFieldPresent(sfIssuerEncryptedBalance))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
bool const hasIssuerAmount = ctx.tx.isFieldPresent(sfIssuerEncryptedAmount);
|
||||
bool const hasAuditorAmount = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
|
||||
|
||||
// Migrating the auditor mirror requires the issuance to have a registered
|
||||
// auditor encryption key.
|
||||
if (hasAuditorAmount && !sleIssuance->isFieldPresent(sfAuditorEncryptionKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Epoch staleness. Mirror epochs count how many times the
|
||||
// holder's mirrors have been re-encrypted; the issuance key epochs count
|
||||
// how many times the keys have rotated.
|
||||
std::uint32_t const issuerKeyEpoch = (*sleIssuance)[~sfIssuerKeyEpoch].value_or(0);
|
||||
std::uint32_t const auditorKeyEpoch = (*sleIssuance)[~sfAuditorKeyEpoch].value_or(0);
|
||||
std::uint32_t const issuerMirrorEpoch = (*sleMptoken)[~sfIssuerKeyMirrorEpoch].value_or(0);
|
||||
std::uint32_t const auditorMirrorEpoch = (*sleMptoken)[~sfAuditorKeyMirrorEpoch].value_or(0);
|
||||
|
||||
// The issuer mirror can only be re-encrypted while it is stale.
|
||||
if (hasIssuerAmount && issuerMirrorEpoch >= issuerKeyEpoch)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (hasAuditorAmount)
|
||||
{
|
||||
// An issuer-mode auditor-only migration: the issuer mirror must already be up to date.
|
||||
if (hasHolder && !hasIssuerAmount && issuerMirrorEpoch != issuerKeyEpoch)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// The auditor mirror can only be re-encrypted while it is stale, unless
|
||||
// this is its first-time registration (no auditor mirror yet).
|
||||
bool const hasAuditorMirror = sleMptoken->isFieldPresent(sfAuditorEncryptedBalance);
|
||||
if (hasAuditorMirror && auditorMirrorEpoch >= auditorKeyEpoch)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// Holder self-migration re-encrypts the mirror from the holder's own
|
||||
// spending balance, which reflects the holder's full balance only once the
|
||||
// inbox has been merged into it. Require the inbox to be canonical zero,
|
||||
// i.e. ConfidentialMPTMergeInbox has already been applied.
|
||||
if (!hasHolder)
|
||||
{
|
||||
// Sanity check: a holder that already carries an issuer mirror
|
||||
// necessarily has a holder encryption key and a spending balance
|
||||
if (!sleMptoken->isFieldPresent(sfHolderEncryptionKey) ||
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::ConfidentialMPTMirrorUpdate::preclaim : an issuer mirror implies a holder "
|
||||
"key and spending balance");
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const expectedZeroInbox = encryptCanonicalZeroAmount(
|
||||
(*sleMptoken)[sfHolderEncryptionKey], holderID, mptIssuanceID);
|
||||
if (!expectedZeroInbox)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::ConfidentialMPTMirrorUpdate::preclaim : canonical zero encryption cannot "
|
||||
"fail for an already-valid holder public key");
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
bool const inboxIsCanonicalZero = sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
|
||||
Slice((*sleMptoken)[sfConfidentialBalanceInbox]) == Slice(*expectedZeroInbox);
|
||||
if (!inboxIsCanonicalZero)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// todo: check zkproof
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTMirrorUpdate::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
|
||||
auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::ConfidentialMPTMirrorUpdate::doApply : preclaim already validated the "
|
||||
"issuance exists");
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// The holderID is sfHolder in issuer mode and sfAccount in holder mode.
|
||||
auto const holder = ctx_.tx[~sfHolder];
|
||||
auto const holderID = holder.value_or(accountID_);
|
||||
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, holderID));
|
||||
if (!sleMptoken)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::ConfidentialMPTMirrorUpdate::doApply : preclaim already validated the "
|
||||
"MPToken exists");
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Re-encrypt the requested mirror(s) and advance the corresponding mirror
|
||||
// epoch to match the issuance key epoch. Only
|
||||
// set the epoch field when it is non-zero, matching the issuance convention
|
||||
// that an absent epoch means zero.
|
||||
if (ctx_.tx.isFieldPresent(sfIssuerEncryptedAmount))
|
||||
{
|
||||
(*sleMptoken)[sfIssuerEncryptedBalance] = ctx_.tx[sfIssuerEncryptedAmount];
|
||||
std::uint32_t const issuerKeyEpoch = (*sleIssuance)[~sfIssuerKeyEpoch].value_or(0);
|
||||
if (issuerKeyEpoch != 0)
|
||||
(*sleMptoken)[sfIssuerKeyMirrorEpoch] = issuerKeyEpoch;
|
||||
}
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfAuditorEncryptedAmount))
|
||||
{
|
||||
(*sleMptoken)[sfAuditorEncryptedBalance] = ctx_.tx[sfAuditorEncryptedAmount];
|
||||
std::uint32_t const auditorKeyEpoch = (*sleIssuance)[~sfAuditorKeyEpoch].value_or(0);
|
||||
if (auditorKeyEpoch != 0)
|
||||
(*sleMptoken)[sfAuditorKeyMirrorEpoch] = auditorKeyEpoch;
|
||||
}
|
||||
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
ConfidentialMPTMirrorUpdate::visitInvariantEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
ConfidentialMPTMirrorUpdate::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2556,4 +2556,52 @@ MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq)
|
||||
return jv;
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::mirrorUpdate(MPTMirrorUpdate const& arg)
|
||||
{
|
||||
json::Value jv;
|
||||
if (arg.account)
|
||||
{
|
||||
jv[sfAccount] = arg.account->human();
|
||||
}
|
||||
else
|
||||
{
|
||||
Throw<std::runtime_error>("Account not specified");
|
||||
}
|
||||
|
||||
if (arg.id)
|
||||
{
|
||||
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!id_)
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
jv[sfMPTokenIssuanceID] = to_string(*id_);
|
||||
}
|
||||
|
||||
jv[sfTransactionType] = jss::ConfidentialMPTMirrorUpdate;
|
||||
|
||||
if (arg.holder)
|
||||
jv[sfHolder] = arg.holder->human();
|
||||
if (arg.issuerEncryptedAmount)
|
||||
jv[sfIssuerEncryptedAmount] = strHex(*arg.issuerEncryptedAmount);
|
||||
if (arg.auditorEncryptedAmount)
|
||||
jv[sfAuditorEncryptedAmount] = strHex(*arg.auditorEncryptedAmount);
|
||||
if (arg.previousIssuerKey)
|
||||
jv[sfPreviousIssuerEncryptionKey] = strHex(*arg.previousIssuerKey);
|
||||
|
||||
// Placeholder for proof, the logic will be added in the future
|
||||
if (arg.zkProof)
|
||||
{
|
||||
jv[sfZKProof] = strHex(*arg.zkProof);
|
||||
}
|
||||
else
|
||||
{
|
||||
jv[sfZKProof] = strHex(gMakeZeroBuffer(kEcGamalEncryptedTotalLength));
|
||||
}
|
||||
|
||||
submit(arg, jv);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test::jtx
|
||||
|
||||
@@ -78,7 +78,7 @@ 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::ConfidentialMPTClawback || txType == jss::ConfidentialMPTMirrorUpdate)
|
||||
{
|
||||
jv[jss::Fee] = to_string(base * (kConfidentialFeeMultiplier + 1));
|
||||
}
|
||||
|
||||
@@ -361,6 +361,25 @@ struct MPTConfidentialClawback
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Arguments for building a ConfidentialMPTMirrorUpdate test transaction.
|
||||
*/
|
||||
struct MPTMirrorUpdate
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
std::optional<Account> holder = std::nullopt;
|
||||
std::optional<MPTID> id = std::nullopt;
|
||||
std::optional<Buffer> issuerEncryptedAmount = std::nullopt;
|
||||
std::optional<Buffer> auditorEncryptedAmount = std::nullopt;
|
||||
std::optional<Buffer> previousIssuerKey = std::nullopt;
|
||||
std::optional<Buffer> zkProof = std::nullopt;
|
||||
std::optional<XRPAmount> fee = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stores the parameters that are exclusively used to generate a
|
||||
* Pedersen linkage proof.
|
||||
@@ -581,6 +600,9 @@ public:
|
||||
void
|
||||
confidentialClaw(MPTConfidentialClawback const& arg = MPTConfidentialClawback{});
|
||||
|
||||
void
|
||||
mirrorUpdate(MPTMirrorUpdate const& arg = MPTMirrorUpdate{});
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkDomainID(std::optional<uint256> expected) const;
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
auto const confidentialBalanceVersionValue = canonical_UINT32();
|
||||
auto const issuerEncryptedBalanceValue = canonical_VL();
|
||||
auto const auditorEncryptedBalanceValue = canonical_VL();
|
||||
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
|
||||
MPTokenBuilder builder{
|
||||
@@ -49,6 +51,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
builder.setConfidentialBalanceVersion(confidentialBalanceVersionValue);
|
||||
builder.setIssuerEncryptedBalance(issuerEncryptedBalanceValue);
|
||||
builder.setAuditorEncryptedBalance(auditorEncryptedBalanceValue);
|
||||
builder.setIssuerKeyMirrorEpoch(issuerKeyMirrorEpochValue);
|
||||
builder.setAuditorKeyMirrorEpoch(auditorKeyMirrorEpochValue);
|
||||
builder.setHolderEncryptionKey(holderEncryptionKeyValue);
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
@@ -146,6 +150,22 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasAuditorEncryptedBalance());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerKeyMirrorEpochValue;
|
||||
auto const actualOpt = entry.getIssuerKeyMirrorEpoch();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfIssuerKeyMirrorEpoch");
|
||||
EXPECT_TRUE(entry.hasIssuerKeyMirrorEpoch());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = auditorKeyMirrorEpochValue;
|
||||
auto const actualOpt = entry.getAuditorKeyMirrorEpoch();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfAuditorKeyMirrorEpoch");
|
||||
EXPECT_TRUE(entry.hasAuditorKeyMirrorEpoch());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = holderEncryptionKeyValue;
|
||||
auto const actualOpt = entry.getHolderEncryptionKey();
|
||||
@@ -179,6 +199,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
auto const confidentialBalanceVersionValue = canonical_UINT32();
|
||||
auto const issuerEncryptedBalanceValue = canonical_VL();
|
||||
auto const auditorEncryptedBalanceValue = canonical_VL();
|
||||
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
|
||||
auto const holderEncryptionKeyValue = canonical_VL();
|
||||
|
||||
auto sle = std::make_shared<SLE>(MPToken::entryType, index);
|
||||
@@ -195,6 +217,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfConfidentialBalanceVersion) = confidentialBalanceVersionValue;
|
||||
sle->at(sfIssuerEncryptedBalance) = issuerEncryptedBalanceValue;
|
||||
sle->at(sfAuditorEncryptedBalance) = auditorEncryptedBalanceValue;
|
||||
sle->at(sfIssuerKeyMirrorEpoch) = issuerKeyMirrorEpochValue;
|
||||
sle->at(sfAuditorKeyMirrorEpoch) = auditorKeyMirrorEpochValue;
|
||||
sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue;
|
||||
|
||||
MPTokenBuilder builderFromSle{sle};
|
||||
@@ -347,6 +371,32 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptedBalance");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerKeyMirrorEpochValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getIssuerKeyMirrorEpoch();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getIssuerKeyMirrorEpoch();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfIssuerKeyMirrorEpoch");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfIssuerKeyMirrorEpoch");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = auditorKeyMirrorEpochValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getAuditorKeyMirrorEpoch();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getAuditorKeyMirrorEpoch();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfAuditorKeyMirrorEpoch");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfAuditorKeyMirrorEpoch");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = holderEncryptionKeyValue;
|
||||
|
||||
@@ -436,6 +486,10 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getIssuerEncryptedBalance().has_value());
|
||||
EXPECT_FALSE(entry.hasAuditorEncryptedBalance());
|
||||
EXPECT_FALSE(entry.getAuditorEncryptedBalance().has_value());
|
||||
EXPECT_FALSE(entry.hasIssuerKeyMirrorEpoch());
|
||||
EXPECT_FALSE(entry.getIssuerKeyMirrorEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasAuditorKeyMirrorEpoch());
|
||||
EXPECT_FALSE(entry.getAuditorKeyMirrorEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasHolderEncryptionKey());
|
||||
EXPECT_FALSE(entry.getHolderEncryptionKey().has_value());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// Auto-generated unit tests for transaction ConfidentialMPTMirrorUpdate
|
||||
|
||||
|
||||
#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/ConfidentialMPTMirrorUpdate.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(TransactionsConfidentialMPTMirrorUpdateTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMirrorUpdate"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const mPTokenIssuanceIDValue = canonical_UINT192();
|
||||
auto const holderValue = canonical_ACCOUNT();
|
||||
auto const issuerEncryptedAmountValue = canonical_VL();
|
||||
auto const auditorEncryptedAmountValue = canonical_VL();
|
||||
auto const previousIssuerEncryptionKeyValue = canonical_VL();
|
||||
auto const zKProofValue = canonical_VL();
|
||||
|
||||
ConfidentialMPTMirrorUpdateBuilder builder{
|
||||
accountValue,
|
||||
mPTokenIssuanceIDValue,
|
||||
zKProofValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
builder.setHolder(holderValue);
|
||||
builder.setIssuerEncryptedAmount(issuerEncryptedAmountValue);
|
||||
builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
|
||||
builder.setPreviousIssuerEncryptionKey(previousIssuerEncryptionKeyValue);
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(tx.validate(reason)) << reason;
|
||||
|
||||
// Verify signing was applied
|
||||
EXPECT_FALSE(tx.getSigningPubKey().empty());
|
||||
EXPECT_TRUE(tx.hasTxnSignature());
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(tx.getAccount(), accountValue);
|
||||
EXPECT_EQ(tx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(tx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = mPTokenIssuanceIDValue;
|
||||
auto const actual = tx.getMPTokenIssuanceID();
|
||||
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = zKProofValue;
|
||||
auto const actual = tx.getZKProof();
|
||||
expectEqualField(expected, actual, "sfZKProof");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = holderValue;
|
||||
auto const actualOpt = tx.getHolder();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolder should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfHolder");
|
||||
EXPECT_TRUE(tx.hasHolder());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerEncryptedAmountValue;
|
||||
auto const actualOpt = tx.getIssuerEncryptedAmount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptedAmount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfIssuerEncryptedAmount");
|
||||
EXPECT_TRUE(tx.hasIssuerEncryptedAmount());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = auditorEncryptedAmountValue;
|
||||
auto const actualOpt = tx.getAuditorEncryptedAmount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
|
||||
EXPECT_TRUE(tx.hasAuditorEncryptedAmount());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousIssuerEncryptionKeyValue;
|
||||
auto const actualOpt = tx.getPreviousIssuerEncryptionKey();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfPreviousIssuerEncryptionKey should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfPreviousIssuerEncryptionKey");
|
||||
EXPECT_TRUE(tx.hasPreviousIssuerEncryptionKey());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsConfidentialMPTMirrorUpdateTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMirrorUpdateFromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const mPTokenIssuanceIDValue = canonical_UINT192();
|
||||
auto const holderValue = canonical_ACCOUNT();
|
||||
auto const issuerEncryptedAmountValue = canonical_VL();
|
||||
auto const auditorEncryptedAmountValue = canonical_VL();
|
||||
auto const previousIssuerEncryptionKeyValue = canonical_VL();
|
||||
auto const zKProofValue = canonical_VL();
|
||||
|
||||
// Build an initial transaction
|
||||
ConfidentialMPTMirrorUpdateBuilder initialBuilder{
|
||||
accountValue,
|
||||
mPTokenIssuanceIDValue,
|
||||
zKProofValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
initialBuilder.setHolder(holderValue);
|
||||
initialBuilder.setIssuerEncryptedAmount(issuerEncryptedAmountValue);
|
||||
initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
|
||||
initialBuilder.setPreviousIssuerEncryptionKey(previousIssuerEncryptionKeyValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
ConfidentialMPTMirrorUpdateBuilder builderFromTx{initialTx.getSTTx()};
|
||||
|
||||
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
|
||||
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = mPTokenIssuanceIDValue;
|
||||
auto const actual = rebuiltTx.getMPTokenIssuanceID();
|
||||
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = zKProofValue;
|
||||
auto const actual = rebuiltTx.getZKProof();
|
||||
expectEqualField(expected, actual, "sfZKProof");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = holderValue;
|
||||
auto const actualOpt = rebuiltTx.getHolder();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolder should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfHolder");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerEncryptedAmountValue;
|
||||
auto const actualOpt = rebuiltTx.getIssuerEncryptedAmount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptedAmount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfIssuerEncryptedAmount");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = auditorEncryptedAmountValue;
|
||||
auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousIssuerEncryptionKeyValue;
|
||||
auto const actualOpt = rebuiltTx.getPreviousIssuerEncryptionKey();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfPreviousIssuerEncryptionKey should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfPreviousIssuerEncryptionKey");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsConfidentialMPTMirrorUpdateTests, 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(ConfidentialMPTMirrorUpdate{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsConfidentialMPTMirrorUpdateTests, 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(ConfidentialMPTMirrorUpdateBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(TransactionsConfidentialMPTMirrorUpdateTests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMirrorUpdateNullopt"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 3;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific required field values
|
||||
auto const mPTokenIssuanceIDValue = canonical_UINT192();
|
||||
auto const zKProofValue = canonical_VL();
|
||||
|
||||
ConfidentialMPTMirrorUpdateBuilder builder{
|
||||
accountValue,
|
||||
mPTokenIssuanceIDValue,
|
||||
zKProofValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Do NOT set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(tx.hasHolder());
|
||||
EXPECT_FALSE(tx.getHolder().has_value());
|
||||
EXPECT_FALSE(tx.hasIssuerEncryptedAmount());
|
||||
EXPECT_FALSE(tx.getIssuerEncryptedAmount().has_value());
|
||||
EXPECT_FALSE(tx.hasAuditorEncryptedAmount());
|
||||
EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value());
|
||||
EXPECT_FALSE(tx.hasPreviousIssuerEncryptionKey());
|
||||
EXPECT_FALSE(tx.getPreviousIssuerEncryptionKey().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user