support ConfidentialMPTMirrorUpdate

This commit is contained in:
yinyiqian1
2026-08-18 11:44:59 -04:00
parent 3e4bdf2782
commit fee81fde05
13 changed files with 2503 additions and 2 deletions

View File

@@ -429,6 +429,8 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfConfidentialBalanceVersion, SoeDefault},
{sfIssuerEncryptedBalance, SoeOptional},
{sfAuditorEncryptedBalance, SoeOptional},
{sfIssuerKeyMirrorEpoch, SoeOptional},
{sfAuditorKeyMirrorEpoch, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
}))

View File

@@ -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)

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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));
}

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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());
}
}