Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/xrpl/protocol/detail/ledger_entries.macro
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
{sfReferenceHolding, SoeOptional},
{sfIssuerEncryptionKey, SoeOptional},
{sfAuditorEncryptionKey, SoeOptional},
{sfIssuerKeyEpoch, SoeOptional},
{sfAuditorKeyEpoch, SoeOptional},
{sfConfidentialOutstandingAmount, SoeDefault},
}))

Expand Down
70 changes: 70 additions & 0 deletions include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,54 @@ class MPTokenIssuance : public LedgerEntryBase
return this->sle_->isFieldPresent(sfAuditorEncryptionKey);
}

/**
* @brief Get sfIssuerKeyEpoch (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getIssuerKeyEpoch() const
{
if (hasIssuerKeyEpoch())
return this->sle_->at(sfIssuerKeyEpoch);
return std::nullopt;
}

/**
* @brief Check if sfIssuerKeyEpoch is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasIssuerKeyEpoch() const
{
return this->sle_->isFieldPresent(sfIssuerKeyEpoch);
}

/**
* @brief Get sfAuditorKeyEpoch (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getAuditorKeyEpoch() const
{
if (hasAuditorKeyEpoch())
return this->sle_->at(sfAuditorKeyEpoch);
return std::nullopt;
}

/**
* @brief Check if sfAuditorKeyEpoch is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorKeyEpoch() const
{
return this->sle_->isFieldPresent(sfAuditorKeyEpoch);
}

/**
* @brief Get sfConfidentialOutstandingAmount (SoeDefault)
* @return The field value, or std::nullopt if not present.
Expand Down Expand Up @@ -600,6 +648,28 @@ class MPTokenIssuanceBuilder : public LedgerEntryBuilderBase<MPTokenIssuanceBuil
return *this;
}

/**
* @brief Set sfIssuerKeyEpoch (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceBuilder&
setIssuerKeyEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfIssuerKeyEpoch] = value;
return *this;
}

/**
* @brief Set sfAuditorKeyEpoch (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceBuilder&
setAuditorKeyEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfAuditorKeyEpoch] = value;
return *this;
}

/**
* @brief Set sfConfidentialOutstandingAmount (SoeDefault)
* @return Reference to this builder for method chaining.
Expand Down
78 changes: 59 additions & 19 deletions src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,15 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
return temMALFORMED;

if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
// Pre-ConfidentialKeyRotation amendment, the auditor key could not be
// registered independently of the issuer key. The issuer could either:
// - Register only the issuer key (in which case an auditor key could not be added later), or
// - Register both the issuer and auditor keys simultaneously.
//
// Post-ConfidentialKeyRotation amendment, the auditor key can be
// registered after the issuer key has already been registered.
if (hasAuditorElGamalKey && !hasIssuerElGamalKey &&
!ctx.rules.enabled(featureConfidentialKeyRotation))
return temMALFORMED;

if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey]))
Expand Down Expand Up @@ -252,45 +260,61 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}

// Updating an existing encryption key requires the
// ConfidentialKeyRotation amendment.
bool const canRotateKey = ctx.view.rules().enabled(featureConfidentialKeyRotation);

bool const txHasIssuerKey = ctx.tx.isFieldPresent(sfIssuerEncryptionKey);
bool const txHasAuditorKey = ctx.tx.isFieldPresent(sfAuditorEncryptionKey);
bool const sleHasIssuerKey = sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey);
bool const sleHasAuditorKey = sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey);

// cannot update issuer public key
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best practices for amendments are using one amendment enabled block and one amendment disabled block, even if they are similar, instead of spreading amendment through the code.

sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey))
{
if (!canRotateKey && txHasIssuerKey && sleHasIssuerKey)
return tecNO_PERMISSION;
}

// cannot update auditor public key
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey))
{
if (!canRotateKey && txHasAuditorKey && sleHasAuditorKey)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}

// A first-time auditor key registration
// requires an issuer key, either already on the issuance or set by the
// same transaction.
bool const registersAuditorKey = txHasAuditorKey && !sleHasAuditorKey;
bool const issuerKeyExists = sleHasIssuerKey || txHasIssuerKey;
if (canRotateKey && registersAuditorKey && !issuerKeyExists)
return tecNO_PERMISSION;

if (enablesConfidentialAmount && sleMptIssuance->isFieldPresent(sfTransferFee) &&
(*sleMptIssuance)[sfTransferFee] > 0u)
return tecNO_PERMISSION;

// Encryption keys can only be set if confidential amounts are already
// enabled on the issuance OR if the transaction is enabling it
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount)
if (txHasIssuerKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
!enablesConfidentialAmount)
{
return tecNO_PERMISSION;
}

if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount)
if (txHasAuditorKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
!enablesConfidentialAmount)
{
return tecNO_PERMISSION;
}

// cannot upload key if there's circulating supply of COA
if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) ||
ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialAmount) &&
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
{
bool const hasConfidentialOA =
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0;

// Pre-ConfidentialKeyRotation amendment, keys cannot be uploaded while
// COA > 0. Post-amendment they can be uploaded even if COA > 0.
if (!canRotateKey && (txHasIssuerKey || txHasAuditorKey) && hasConfidentialOA)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}

// Enabling confidential balances when COA > 0 is not permitted, regardless of
// ConfidentialKeyRotation.
if (enablesConfidentialAmount && hasConfidentialOA)
return tecNO_PERMISSION;

return tesSUCCESS;
}
Expand Down Expand Up @@ -397,7 +421,16 @@ MPTokenIssuanceSet::doApply()
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");

// Only increment the issuer key epoch on a rotation. A first-time registration leaves
// it absent (epoch 0), matching issuances whose keys were registered
// before the ConfidentialKeyRotation amendment.
// NOTE: presence must be checked before the key is overwritten below.
bool const isRotation = sle->isFieldPresent(sfIssuerEncryptionKey);

sle->setFieldVL(sfIssuerEncryptionKey, *pubKey);

if (isRotation)
(*sle)[sfIssuerKeyEpoch] = (*sle)[~sfIssuerKeyEpoch].valueOr(0) + 1;
}

if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey])
Expand All @@ -407,7 +440,14 @@ MPTokenIssuanceSet::doApply()
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");

// Only increment the auditor key epoch on a rotation. First time registration leaves it
// absent.
bool const isRotation = sle->isFieldPresent(sfAuditorEncryptionKey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 have the same code, why not make helper. or lambda?


sle->setFieldVL(sfAuditorEncryptionKey, *pubKey);

if (isRotation)
(*sle)[sfAuditorKeyEpoch] = (*sle)[~sfAuditorKeyEpoch].valueOr(0) + 1;
}

view().update(sle);
Expand Down
Loading
Loading