Compare commits

..

8 Commits

Author SHA1 Message Date
yinyiqian1
4dacf95a40 update 2026-09-15 17:31:22 -04:00
yinyiqian1
f489f8b277 update 2026-09-15 16:40:12 -04:00
yinyiqian1
d7443802ef add test 2026-09-15 16:33:26 -04:00
yinyiqian1
dc25085aa7 update 2026-09-15 16:27:49 -04:00
yinyiqian1
c555ed7947 autogen 2026-09-15 13:54:48 -04:00
yinyiqian1
74500969b4 Merge branch 'develop' into yinyi/mirror-update 2026-09-15 12:35:57 -04:00
yinyiqian1
7f169ef864 update 2026-09-08 14:52:03 -04:00
yinyiqian1
fee81fde05 support ConfidentialMPTMirrorUpdate 2026-09-08 13:05:16 -04:00
20 changed files with 3060 additions and 646 deletions

View File

@@ -347,6 +347,30 @@ isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken);
[[nodiscard]] bool
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken);
/**
* @brief Set the holder's issuer mirror epoch to match the issuance's current issuer key epoch.
*
* Call this after writing the issuer mirror ciphertext under the issuance's
* currently registered issuer key, so that the mirror reads as current afterwards.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger entry to update.
*/
void
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken);
/**
* @brief Set the holder's auditor mirror epoch to match the issuance's current auditor key epoch.
*
* Call this after writing the auditor mirror ciphertext under the issuance's
* currently registered auditor key. Does nothing when the holder has no auditor mirror.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger entry to update.
*/
void
setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken);
/**
* @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
*

View File

@@ -540,6 +540,11 @@ constexpr std::size_t kEcConvertBackProofLength =
*/
constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE;
/**
* Length of compact equality proof.
*/
constexpr std::size_t kEcEqualityProofLength = 128;
/**
* Extra base fee multiplier charged to confidential MPT transactions.
*/

View File

@@ -1134,6 +1134,19 @@ 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,
({.delegable = Delegation::Delegable, .amendment = featureConfidentialMPTKeyRotation}),
({
{sfMPTokenIssuanceID, SoeRequired},
{sfHolder, SoeOptional},
{sfIssuerEncryptedAmount, SoeOptional},
{sfAuditorEncryptedAmount, 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

@@ -0,0 +1,266 @@
// 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::Delegable
* Amendment: featureConfidentialMPTKeyRotation
* Privileges: Privilege::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 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 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,103 @@
#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. 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). This mode is always
* available to the holder and is not conditioned on the issuer being unable to migrate
* them: the ledger cannot verify whether an issuer has really lost its private key. That
* loss is only the expected motivation, since an issuer that still holds its key can
* migrate holders itself in issuer mode.
* @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. This is the
* holder-driven counterpart to flows 2 and 4, for when the issuer does not migrate
* the holder itself.
*
* 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

@@ -435,21 +435,41 @@ areMirrorsCurrent(SLE const& issuance, SLE const& mptoken)
}
void
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::setMirrorEpochs : issuance MPTokenIssuance object");
XRPL_ASSERT(mptoken.getType() == ltMPTOKEN, "xrpl::setMirrorEpochs : mptoken MPToken object");
"xrpl::setIssuerMirrorEpoch : issuance MPTokenIssuance object");
XRPL_ASSERT(
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorEpoch : mptoken MPToken object");
// Unlike the auditor mirror, the issuer mirror is not optional: every
// confidential MPToken carries one, so there is no existence check here.
if (auto const epoch = issuance[~sfIssuerKeyEpoch].value_or(0); epoch != 0)
mptoken[sfIssuerKeyMirrorEpoch] = epoch;
}
if (mptoken.isFieldPresent(sfAuditorEncryptedBalance))
{
if (auto const epoch = issuance[~sfAuditorKeyEpoch].value_or(0); epoch != 0)
mptoken[sfAuditorKeyMirrorEpoch] = epoch;
}
void
setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::setAuditorMirrorEpoch : issuance MPTokenIssuance object");
XRPL_ASSERT(
mptoken.getType() == ltMPTOKEN, "xrpl::setAuditorMirrorEpoch : mptoken MPToken object");
if (!mptoken.isFieldPresent(sfAuditorEncryptedBalance))
return;
if (auto const epoch = issuance[~sfAuditorKeyEpoch].value_or(0); epoch != 0)
mptoken[sfAuditorKeyMirrorEpoch] = epoch;
}
void
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
{
setIssuerMirrorEpoch(issuance, mptoken);
setAuditorMirrorEpoch(issuance, mptoken);
}
TER

View File

@@ -0,0 +1,274 @@
#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 <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;
// 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;
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;
// An issuer mirror may only be re-encrypted while it is stale, reject if it is already current.
if (hasIssuerAmount && isIssuerMirrorCurrent(*sleIssuance, *sleMptoken))
return tecNO_PERMISSION;
if (hasAuditorAmount)
{
// An issuer-mode auditor-only migration: the issuer mirror must already be up to date.
if (hasHolder && !hasIssuerAmount && !isIssuerMirrorCurrent(*sleIssuance, *sleMptoken))
return tecNO_PERMISSION;
// An auditor mirror may only be re-encrypted while it is stale, reject if it is already
// current. isAuditorMirrorCurrent reports an absent auditor mirror as stale, which is what
// allows an auditor-only migration to create one for the first time.
if (isAuditorMirrorCurrent(*sleIssuance, *sleMptoken))
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;
}
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. Each mirror is stamped separately
// because this transaction may migrate either one or both.
if (ctx_.tx.isFieldPresent(sfIssuerEncryptedAmount))
{
(*sleMptoken)[sfIssuerEncryptedBalance] = ctx_.tx[sfIssuerEncryptedAmount];
setIssuerMirrorEpoch(*sleIssuance, *sleMptoken);
}
if (ctx_.tx.isFieldPresent(sfAuditorEncryptedAmount))
{
(*sleMptoken)[sfAuditorEncryptedBalance] = ctx_.tx[sfAuditorEncryptedAmount];
setAuditorMirrorEpoch(*sleIssuance, *sleMptoken);
}
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

@@ -2191,7 +2191,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.account = bob,
.dest = bob,
.amt = 10,
.proof = getTrivialSendProofHex(),
.err = temMALFORMED,
});
@@ -2898,6 +2897,22 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
auto& mptAlice = confEnv.mpt;
{
// Bob has 60, tries to send 70. Invalid remaining balance.
mptAlice.send({
.account = bob,
.dest = carol,
.amt = 70,
.err = tecBAD_PROOF,
});
// Bob has 60, tries to send 61. Invalid remaining balance.
mptAlice.send({
.account = bob,
.dest = carol,
.amt = 61,
.err = tecBAD_PROOF,
});
// Bob has 60, sends 60. Remainder is exactly 0. Valid remaining balance.
mptAlice.send({
.account = bob,
@@ -2918,12 +2933,12 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
});
// Bob has 100, tries to send 2^64-1. Invalid remaining balance.
{
ConfidentialSendSetup const setup(
mptAlice, bob, carol, alice, std::numeric_limits<std::uint64_t>::max());
auto const forged = getForgedSendProof(mptAlice, env, bob, carol, setup);
mptAlice.send(setup.sendArgs(bob, carol, forged, tecBAD_PROOF));
}
mptAlice.send({
.account = bob,
.dest = carol,
.amt = std::numeric_limits<std::uint64_t>::max(),
.err = tecBAD_PROOF,
});
// Bob sends 1, remaining 99.
mptAlice.send({
@@ -2932,6 +2947,14 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.amt = 1,
.err = tesSUCCESS,
});
// Bob sends 100, but only has 99. Invalid remaining balance.
mptAlice.send({
.account = bob,
.dest = carol,
.amt = 100,
.err = tecBAD_PROOF,
});
}
// send when spending balance is 0 (key registered, inbox merged, but nothing converted)
@@ -2948,13 +2971,18 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
// Trying to send any amount with 0 spending balance must fail:
// the range proof for < 0 is invalid.
ConfidentialSendSetup const setup(mptAlice2, bob2, carol2, alice2, 1);
auto const forged = getForgedSendProof(mptAlice2, env2, bob2, carol2, setup);
mptAlice2.send(setup.sendArgs(bob2, carol2, forged, tecBAD_PROOF));
mptAlice2.send({
.account = bob2,
.dest = carol2,
.amt = 1,
.err = tecBAD_PROOF,
});
BEAST_EXPECT(
mptAlice2.getDecryptedBalance(bob2, MPTTester::holderEncryptedSpending) == 0);
}
// todo: test m exceeding range, require using scala and refactor
}
/* The equality proof library and range proof library do not
@@ -3434,7 +3462,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const convertBackContextHash =
getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
convertBackAmt,
convertBackContextHash,
@@ -3444,8 +3472,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
{
json::Value jv;
@@ -3457,7 +3483,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
jv[sfIssuerEncryptedAmount.jsonName] = strHex(convertBackIssuerCiphertext);
jv[sfBlindingFactor.jsonName] = strHex(convertBackBlindingFactor);
jv[sfBalanceCommitment.jsonName] = strHex(pedersenCommitment);
jv[sfZKProof.jsonName] = strHex(requireOptionalRef(proof, "Missing proof"));
jv[sfZKProof.jsonName] = strHex(proof);
env(jv, Ter(tesSUCCESS));
}
@@ -5257,7 +5283,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
Buffer const badPedersenCommitment =
mptAlice.getPedersenCommitment(1, pcBlindingFactor);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
@@ -5267,8 +5293,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -5289,7 +5313,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
@@ -5299,8 +5323,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = generateBlindingFactor(), // wrong blinding factor
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -5315,26 +5337,22 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
}
// Test 3: Proof generated with wrong balance value.
// The sigma proof claims balance=20 but the pedersen commitment and
// encrypted spending balance were built for the actual balance (40).
// we cannot call mpt_get_convert_back_proof because it has client-side
// verification.
// The proof claims balance=1 but the encrypted spending balance contains
// the actual balance. Verification fails because the values don't match.
{
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
uint64_t constexpr claimedBalance = 20; // wrong: real balance is 40
auto const proof = getForgedConvertBackProof(
mptAlice,
Buffer const proof = mptAlice.getConvertBackProof(
bob,
claimedBalance,
spendingBalance,
amt,
pedersenCommitment,
encryptedSpendingBalance,
pcBlindingFactor,
contextHash);
contextHash,
{
.pedersenCommitment = pedersenCommitment,
.amt = 1, // wrong balance
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
mptAlice.convertBack({
.account = bob,
@@ -5357,7 +5375,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
Buffer const badPedersenCommitment =
mptAlice.getPedersenCommitment(1, pcBlindingFactor);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
@@ -5367,8 +5385,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -5389,7 +5405,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
{
uint256 const badContextHash{1};
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
badContextHash, // wrong context hash
@@ -5399,8 +5415,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -5420,7 +5434,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
@@ -5430,8 +5444,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -5907,26 +5919,22 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
// linkage, and that the remaining balance is non-negative.
// Test 1: Proof generated with wrong balance value.
// The sigma proof claims balance=20 but the pedersen commitment and
// encrypted spending balance were built for the actual balance (40).
// we cannot call mpt_get_convert_back_proof because it has client-side
// verification.
// The sigma proof claims balance=1 but the spending balance contains the
// actual balance. The compact proof's balance-linkage check fails.
{
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
uint64_t constexpr claimedBalance = 20; // wrong: real balance is 40
auto const proof = getForgedConvertBackProof(
mptAlice,
Buffer const proof = mptAlice.getConvertBackProof(
bob,
claimedBalance,
spendingBalance,
amt,
pedersenCommitment,
encryptedSpendingBalance,
pcBlindingFactor,
contextHash);
contextHash,
{
.pedersenCommitment = pedersenCommitment,
.amt = 1, // wrong balance (actual balance is ~40)
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
mptAlice.convertBack({
.account = bob,
@@ -5948,7 +5956,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
@@ -5958,8 +5966,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = generateBlindingFactor(), // wrong blinding factor
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -5979,7 +5985,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
// makes the proof invalid for this transaction, preventing replay attacks.
{
uint256 const badContextHash{1};
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
badContextHash, // wrong context hash
@@ -5989,8 +5995,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -6010,7 +6014,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
@@ -6020,8 +6024,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -6071,7 +6073,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
auto const version = mptAlice.getMPTokenVersion(bob);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
makeContextHash(env, mptAlice, alice, bob, carol, version),
@@ -6082,8 +6084,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
encryptedSpendingBalance, "Missing encrypted spending balance"),
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -6173,7 +6173,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const contextHashA =
getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, version);
auto const proofA = mptAlice.getConvertBackProof(
Buffer const proofA = mptAlice.getConvertBackProof(
bob,
amtA,
contextHashA,
@@ -6183,8 +6183,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(proofA.has_value()))
return;
// Construct Transaction B with Amount m2 = 20 and attach Proof pi
uint64_t const amtB = 20;
@@ -6256,7 +6254,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const oldContextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, versionV);
auto const oldProof = mptAlice.getConvertBackProof(
Buffer const oldProof = mptAlice.getConvertBackProof(
bob,
amt,
oldContextHash,
@@ -6266,8 +6264,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpendingBalanceV,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(oldProof.has_value()))
return;
// Submit and verify failure
mptAlice.convertBack({
@@ -6330,7 +6326,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
@@ -6340,8 +6336,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = spendingBalEnc,
.blindingFactor = pcBf,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
// Submit transaction with Divergent Ciphertexts
// Holder Ciphertext encrypts 11. Issuer Ciphertext encrypts 10.
@@ -6475,7 +6469,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion);
auto const proof = mptAlice.getConvertBackProof(
Buffer const proof = mptAlice.getConvertBackProof(
bob,
1,
contextHash,
@@ -6485,8 +6479,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = underflowedCt,
.blindingFactor = pcBf,
});
if (!BEAST_EXPECT(proof.has_value()))
return;
mptAlice.convertBack({
.account = bob,
@@ -7749,7 +7741,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
uint256 const convertBackCtxHash =
getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version);
auto const convertBackProof = mptAlice.getConvertBackProof(
Buffer const convertBackProof = mptAlice.getConvertBackProof(
bob,
sendAmount,
convertBackCtxHash,
@@ -7759,18 +7751,14 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.encryptedAmt = encryptedSpending,
.blindingFactor = pcBlindingFactor,
});
if (!BEAST_EXPECT(convertBackProof.has_value()))
return;
// Resize the convertBack proof to match the expected send proof
// size so it passes preflight's size check and reaches the actual
// ZK verification in doApply.
auto const expectedSendSize = kEcSendProofLength;
Buffer resizedProof(expectedSendSize);
Buffer const& convertBackProofRef =
requireOptionalRef(convertBackProof, "Missing proof");
auto const copyLen = std::min(convertBackProofRef.size(), expectedSendSize);
std::memcpy(resizedProof.data(), convertBackProofRef.data(), copyLen);
auto const copyLen = std::min(convertBackProof.size(), expectedSendSize);
std::memcpy(resizedProof.data(), convertBackProof.data(), copyLen);
// Zero-pad the rest (if convertBack proof is shorter)
if (copyLen < expectedSendSize)
std::memset(resizedProof.data() + copyLen, 0, expectedSendSize - copyLen);

View File

@@ -2756,7 +2756,7 @@ class Delegate_test : public beast::unit_test::Suite
// DO NOT modify expectedDelegableCount unless all scenarios, including
// edge cases, have been fully tested and verified.
// ====================================================================
std::size_t const expectedDelegableCount = 56;
std::size_t const expectedDelegableCount = 57;
BEAST_EXPECTS(
delegableCount == expectedDelegableCount,

View File

@@ -124,87 +124,6 @@ protected:
return proof;
}
// Forges a ConvertBack proof (compact sigma + single bulletproof) whose
// sigma component claims claimedBalance (which may be wrong) while binding
// to the real pedersen commitment and encrypted spending balance
// ciphertext already on the ledger. The bulletproof component is built
// from realBalance so it stays honest.
// mpt_get_convert_back_proof does not allow to build a proof whose amount
// exceeds the holder's claimed balance.
static Buffer
getForgedConvertBackProof(
test::jtx::MPTTester& mpt,
test::jtx::Account const& holder,
uint64_t claimedBalance,
uint64_t realBalance,
uint64_t amt,
Buffer const& pedersenCommitment,
Buffer const& encryptedSpendingBalance,
Buffer const& pcBlindingFactor,
uint256 const& contextHash)
{
if (pedersenCommitment.size() != kCompressedEcPointLength)
Throw<std::runtime_error>("getForgedConvertBackProof: bad pedersenCommitment length");
if (encryptedSpendingBalance.size() != kEcGamalEncryptedTotalLength)
{
Throw<std::runtime_error>(
"getForgedConvertBackProof: bad encryptedSpendingBalance length");
}
if (amt > realBalance)
Throw<std::runtime_error>("getForgedConvertBackProof: amt exceeds realBalance");
auto* const ctx = mpt_secp256k1_context();
auto const holderPubKey = requireOptional(mpt.getPubKey(holder), "Missing holder pubkey");
auto const holderPrivKey =
requireOptional(mpt.getPrivKey(holder), "Missing holder privkey");
secp256k1_pubkey pkHolder;
if (secp256k1_ec_pubkey_parse(
ctx, &pkHolder, holderPubKey.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse holder's public key");
secp256k1_pubkey pcB;
if (secp256k1_ec_pubkey_parse(
ctx, &pcB, pedersenCommitment.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse pedersen commitment");
secp256k1_pubkey b1, b2;
if (secp256k1_ec_pubkey_parse(
ctx, &b1, encryptedSpendingBalance.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx,
&b2,
encryptedSpendingBalance.data() + kCompressedEcPointLength,
kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse balance ciphertext");
Buffer sigmaProof(SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE);
if (secp256k1_compact_convertback_prove(
ctx,
sigmaProof.data(),
claimedBalance,
holderPrivKey.data(),
pcBlindingFactor.data(),
&pkHolder,
&b1,
&b2,
&pcB,
contextHash.data()) != 1)
Throw<std::runtime_error>("Failed to generate convertback sigma proof");
auto const forgedBulletproof =
getForgedSingleBulletproof(realBalance - amt, pcBlindingFactor, contextHash);
Buffer proof(kEcConvertBackProofLength);
std::memcpy(proof.data(), sigmaProof.data(), SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE);
std::memcpy(
proof.data() + SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE,
forgedBulletproof.data(),
kEcSingleBulletproofLength);
return proof;
}
// Get a bad ciphertext with valid structure but cryptographic invalid for
// testing purposes. For preflight test purposes.
static Buffer const&
@@ -428,111 +347,6 @@ protected:
}
};
// Forges a ConfidentialMPTSend proof (compact sigma + double bulletproof)
// for setup.sendAmount against setup's real balance commitment/ciphertext.
// mpt_get_confidential_send_proof does not allow to build a proof whose amount
// exceeds the sender's claimed balance.
static Buffer
getForgedSendProof(
test::jtx::MPTTester& mpt,
test::jtx::Env& env,
test::jtx::Account const& sender,
test::jtx::Account const& dest,
ConfidentialSendSetup const& setup)
{
auto* const ctx = mpt_secp256k1_context();
secp256k1_pubkey c1;
std::vector<secp256k1_pubkey> c2Vec(setup.recipients.size());
std::vector<secp256k1_pubkey> pkVec(setup.recipients.size());
for (std::size_t i = 0; i < setup.recipients.size(); ++i)
{
auto const& r = setup.recipients[i];
if (i == 0 &&
secp256k1_ec_pubkey_parse(
ctx, &c1, r.encryptedAmount.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse C1");
if (secp256k1_ec_pubkey_parse(
ctx,
&c2Vec[i],
r.encryptedAmount.data() + kCompressedEcPointLength,
kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse C2");
if (secp256k1_ec_pubkey_parse(
ctx, &pkVec[i], r.publicKey.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse recipient pubkey");
}
secp256k1_pubkey pkSender, pcAmount, pcBalance, b1, b2;
if (secp256k1_ec_pubkey_parse(
ctx, &pkSender, setup.senderPubKey.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx, &pcAmount, setup.amountCommitment.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx, &pcBalance, setup.balanceCommitment.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx, &b1, setup.prevEncryptedSpending.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx,
&b2,
setup.prevEncryptedSpending.data() + kCompressedEcPointLength,
kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse commitments/ciphertext");
Buffer const senderPrivKey =
requireOptional(mpt.getPrivKey(sender), "Missing sender privkey");
auto const ctxHash = getSendContextHash(
sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), setup.version);
Buffer sigmaProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE);
if (secp256k1_compact_standard_prove(
ctx,
sigmaProof.data(),
setup.sendAmount,
setup.prevSpending,
setup.blindingFactor.data(),
senderPrivKey.data(),
setup.balanceBlindingFactor.data(),
setup.recipients.size(),
&c1,
c2Vec.data(),
pkVec.data(),
&pcAmount,
&pkSender,
&pcBalance,
&b1,
&b2,
ctxHash.data()) != 1)
Throw<std::runtime_error>("Failed to generate sigma proof");
// Wraps (mod 2^64) for overdrafts, unlike the ledger's own homomorphic
// commitment subtraction (mod the curve order) — that mismatch is
// exactly what makes the forged proof fail verification.
// Computed without a wrapping `uint64` subtract: Clang UBSan treats
// unsigned overflow as fatal (see incrementConfidentialVersion).
std::uint64_t const remaining = setup.sendAmount <= setup.prevSpending
? setup.prevSpending - setup.sendAmount
: ~setup.sendAmount + setup.prevSpending + 1;
Buffer negAmountBf(kEcBlindingFactorLength);
Buffer remainingBf(kEcBlindingFactorLength);
secp256k1_mpt_scalar_negate(negAmountBf.data(), setup.amountBlindingFactor.data());
secp256k1_mpt_scalar_add(
remainingBf.data(), setup.balanceBlindingFactor.data(), negAmountBf.data());
auto const forgedBulletproof = getForgedBulletproof(
{setup.sendAmount, remaining}, {setup.amountBlindingFactor, remainingBf}, ctxHash);
Buffer combinedProof(kEcSendProofLength);
std::memcpy(combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_STANDARD_PROOF_SIZE);
std::memcpy(
combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE,
forgedBulletproof.data(),
kEcDoubleBulletproofLength);
return combinedProof;
}
// Helper that wraps the boilerplate setup: Env + MPT creation, funding, key
// generation, and seeding each holder with a confidential balance.
// The caller supplies the issuer and any number of holders.

File diff suppressed because it is too large Load Diff

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

@@ -362,6 +362,24 @@ 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> 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.
@@ -584,6 +602,9 @@ public:
void
confidentialClaw(MPTConfidentialClawback const& arg = MPTConfidentialClawback{});
void
mirrorUpdate(MPTMirrorUpdate const& arg = MPTMirrorUpdate{});
[[nodiscard]] bool
checkDomainID(std::optional<uint256> expected) const;
@@ -742,7 +763,7 @@ public:
PedersenProofParams const& amountParams,
PedersenProofParams const& balanceParams) const;
[[nodiscard]] std::optional<Buffer>
[[nodiscard]] Buffer
getConvertBackProof(
Account const& holder,
std::uint64_t const amount,
@@ -841,28 +862,15 @@ private:
[[nodiscard]] std::uint32_t
getFlags(std::optional<Account> const& holder) const;
/**
* @brief Sets sfMPTokenIssuanceID on jv, falling back to id_ if arg's id is
* not set.
*
* @param jv The JSON object to set the field on.
* @param id The explicit issuance ID override from the caller, if any.
*/
void
setIssuanceIdField(json::Value& jv, std::optional<MPTID> const& id) const;
[[nodiscard]] std::uint32_t
ticketOrSeq(
std::optional<std::uint32_t> const& ticketSeq,
std::optional<Account> const& account) const;
template <typename T>
void
fillConversionCiphertexts(
T const& arg,
json::Value& jv,
Account const& account,
std::uint64_t const amount) const;
Buffer& holderCiphertext,
Buffer& issuerCiphertext,
std::optional<Buffer>& auditorCiphertext,
Buffer& blindingFactor) const;
};
} // namespace xrpl::test::jtx

View File

@@ -13,7 +13,6 @@
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <vector>
@@ -97,29 +96,6 @@ public:
}
}
{
testcase("Membership: isMember agrees with member");
// Number of network nodes that also belong to the cluster.
std::size_t const overlapCount = 16;
// Total size of the cluster once padded with non-network nodes.
std::size_t const clusterSize = 32;
std::vector<PublicKey> cluster(network.begin(), network.begin() + overlapCount);
while (cluster.size() != clusterSize)
cluster.push_back(randomNode());
auto c = create(cluster);
for (auto const& n : cluster)
BEAST_EXPECT(c->isMember(n));
for (auto const& n : network)
BEAST_EXPECT(c->isMember(n) == static_cast<bool>(c->member(n)));
}
{
testcase("Membership: Non-empty cluster and all present");

View File

@@ -0,0 +1,255 @@
// 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 zKProofValue = canonical_VL();
ConfidentialMPTMirrorUpdateBuilder builder{
accountValue,
mPTokenIssuanceIDValue,
zKProofValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setHolder(holderValue);
builder.setIssuerEncryptedAmount(issuerEncryptedAmountValue);
builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = mPTokenIssuanceIDValue;
auto const actual = tx.getMPTokenIssuanceID();
expectEqualField(expected, actual, "sfMPTokenIssuanceID");
}
{
auto const& expected = 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());
}
}
// 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 zKProofValue = canonical_VL();
// Build an initial transaction
ConfidentialMPTMirrorUpdateBuilder initialBuilder{
accountValue,
mPTokenIssuanceIDValue,
zKProofValue,
sequenceValue,
feeValue
};
initialBuilder.setHolder(holderValue);
initialBuilder.setIssuerEncryptedAmount(issuerEncryptedAmountValue);
initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
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");
}
}
// 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());
}
}

View File

@@ -62,19 +62,6 @@ public:
std::optional<std::string>
member(PublicKey const& node) const;
/**
* Determines whether a node belongs in the cluster.
*
* Prefer this to `member` when the comment is not wanted: `member`
* copies the node's name out from under the lock, and most callers
* only test the result for engagement.
*
* @param node The node's public identity.
* @return Whether the node is a cluster member.
*/
bool
isMember(PublicKey const& node) const;
/**
* The number of nodes in the cluster list.
*/

View File

@@ -38,14 +38,6 @@ Cluster::member(PublicKey const& identity) const
return iter->name();
}
bool
Cluster::isMember(PublicKey const& identity) const
{
std::scoped_lock const lock(mutex_);
return nodes_.contains(identity);
}
std::size_t
Cluster::size() const
{

View File

@@ -306,7 +306,7 @@ OverlayImpl::onHandoff(
{
// The node gets a reserved slot if it is in our cluster
// or if it has a reservation.
bool const reserved = app_.getCluster().isMember(publicKey) ||
bool const reserved = static_cast<bool>(app_.getCluster().member(publicKey)) ||
app_.getPeerReservations().contains(publicKey);
auto const result = peerFinder_->activate(slot, publicKey, reserved);
if (result != peer_finder::Result::Success)

View File

@@ -413,7 +413,7 @@ PeerImp::crawl() const
bool
PeerImp::cluster() const
{
return app_.getCluster().isMember(publicKey_);
return static_cast<bool>(app_.getCluster().member(publicKey_));
}
std::string