mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-04 10:48:15 +00:00
Merge remote-tracking branch 'upstream/develop' into copilot/add-augmented-submit-fields
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -544,6 +545,11 @@ constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_
|
||||
*/
|
||||
constexpr std::uint32_t kConfidentialFeeMultiplier = 9;
|
||||
|
||||
/**
|
||||
* Maximum value a confidential MPT key epoch may reach.
|
||||
*/
|
||||
constexpr std::uint32_t kMaxKeyEpoch = std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
/**
|
||||
* Compressed EC point prefix for even y-coordinate
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -408,6 +408,8 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfReferenceHolding, SoeOptional},
|
||||
{sfIssuerEncryptionKey, SoeOptional},
|
||||
{sfAuditorEncryptionKey, SoeOptional},
|
||||
{sfIssuerKeyEpoch, SoeOptional},
|
||||
{sfAuditorKeyEpoch, SoeOptional},
|
||||
{sfConfidentialOutstandingAmount, SoeDefault},
|
||||
}))
|
||||
|
||||
|
||||
@@ -119,6 +119,10 @@ TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73)
|
||||
TYPED_SFIELD(sfSponsorFlags, UINT32, 74)
|
||||
TYPED_SFIELD(sfSubscriptionDate, UINT32, 75)
|
||||
TYPED_SFIELD(sfRedemptionDate, UINT32, 76)
|
||||
TYPED_SFIELD(sfIssuerKeyEpoch, UINT32, 77)
|
||||
TYPED_SFIELD(sfAuditorKeyEpoch, UINT32, 78)
|
||||
TYPED_SFIELD(sfIssuerKeyMirrorEpoch, UINT32, 79)
|
||||
TYPED_SFIELD(sfAuditorKeyMirrorEpoch, UINT32, 80)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
|
||||
@@ -351,6 +351,54 @@ public:
|
||||
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.
|
||||
@@ -600,6 +648,28 @@ public:
|
||||
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.
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace {
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
char const* const versionString = "3.4.0-b3"
|
||||
char const* const versionString = "3.4.0-rc1"
|
||||
// clang-format on
|
||||
;
|
||||
|
||||
|
||||
@@ -119,7 +119,15 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
|
||||
// Pre-ConfidentialMPTKeyRotation 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-ConfidentialMPTKeyRotation amendment, the auditor key can be
|
||||
// registered after the issuer key has already been registered.
|
||||
if (hasAuditorElGamalKey && !hasIssuerElGamalKey &&
|
||||
!ctx.rules.enabled(featureConfidentialMPTKeyRotation))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey]))
|
||||
@@ -219,18 +227,57 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// cannot update issuer public key
|
||||
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
|
||||
sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
// Updating an existing encryption key requires the
|
||||
// ConfidentialMPTKeyRotation amendment.
|
||||
bool const canRotateKey = ctx.view.rules().enabled(featureConfidentialMPTKeyRotation);
|
||||
|
||||
// cannot update auditor public key
|
||||
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
|
||||
sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey))
|
||||
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);
|
||||
|
||||
if (canRotateKey)
|
||||
{
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
// Post-ConfidentialMPTKeyRotation amendment, the encryption keys can be updated.
|
||||
// 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 (registersAuditorKey && !issuerKeyExists)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Rotating a key to its current value is not permitted: a key epoch
|
||||
// increment must always correspond to an actual key change.
|
||||
if (txHasIssuerKey && sleHasIssuerKey &&
|
||||
ctx.tx[sfIssuerEncryptionKey] == (*sleMptIssuance)[sfIssuerEncryptionKey])
|
||||
return tecDUPLICATE;
|
||||
|
||||
if (txHasAuditorKey && sleHasAuditorKey &&
|
||||
ctx.tx[sfAuditorEncryptionKey] == (*sleMptIssuance)[sfAuditorEncryptionKey])
|
||||
return tecDUPLICATE;
|
||||
|
||||
// Key epochs must never wrap. Epoch 0 serves as the sentinel for "never
|
||||
// rotated." Holders' mirror epochs are checked against it for equality,
|
||||
// so a wrap would cause stale mirror ciphertexts to appear valid instead
|
||||
// of failing loudly.
|
||||
if (txHasIssuerKey && sleHasIssuerKey &&
|
||||
(*sleMptIssuance)[~sfIssuerKeyEpoch].value_or(0) == kMaxKeyEpoch)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (txHasAuditorKey && sleHasAuditorKey &&
|
||||
(*sleMptIssuance)[~sfAuditorKeyEpoch].value_or(0) == kMaxKeyEpoch)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pre-ConfidentialMPTKeyRotation amendment, the encryption keys can not be updated.
|
||||
// cannot update issuer public key
|
||||
if (txHasIssuerKey && sleHasIssuerKey)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// cannot update auditor public key
|
||||
if (txHasAuditorKey && sleHasAuditorKey)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
auto const enablesConfidentialBalance =
|
||||
@@ -241,25 +288,30 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
// 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) && !enablesConfidentialBalance)
|
||||
if (txHasIssuerKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
|
||||
!enablesConfidentialBalance)
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
|
||||
!sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialBalance)
|
||||
if (txHasAuditorKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
|
||||
!enablesConfidentialBalance)
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// cannot upload key if there's circulating supply of COA
|
||||
if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) ||
|
||||
ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialBalance) &&
|
||||
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
|
||||
{
|
||||
bool const hasConfidentialOA =
|
||||
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0;
|
||||
|
||||
// Pre-ConfidentialMPTKeyRotation 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
|
||||
// ConfidentialMPTKeyRotation.
|
||||
if (enablesConfidentialBalance && hasConfidentialOA)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -377,25 +429,69 @@ MPTokenIssuanceSet::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const pubKey = ctx_.tx[~sfIssuerEncryptionKey])
|
||||
{
|
||||
// This is enforced in preflight.
|
||||
// Sets an encryption key on the issuance. Overwriting an existing key
|
||||
// (a rotation) increments the corresponding key epoch; a first-time
|
||||
// registration leaves the epoch absent (epoch 0), matching issuances
|
||||
// whose keys were registered before the ConfidentialMPTKeyRotation
|
||||
// amendment.
|
||||
bool const canRotateKey = view().rules().enabled(featureConfidentialMPTKeyRotation);
|
||||
auto const setEncryptionKey = [&](SF_VL const& keyField, SF_UINT32 const& epochField) -> TER {
|
||||
auto const pubKey = ctx_.tx[~keyField];
|
||||
if (!pubKey)
|
||||
return tesSUCCESS;
|
||||
|
||||
// This is enforced in preflight, which rejects a transaction carrying
|
||||
// both sfHolder and an encryption key.
|
||||
XRPL_ASSERT(
|
||||
sle->getType() == ltMPTOKEN_ISSUANCE,
|
||||
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
|
||||
sle->setFieldVL(sfIssuerEncryptionKey, *pubKey);
|
||||
}
|
||||
// Add sanity check under the amendment ConfidentialMPTKeyRotation.
|
||||
// Pre-confidentialMPTKeyRotation did not return tecINTERNAL so
|
||||
// this should be under the amendment guard.
|
||||
if (canRotateKey && sle->getType() != ltMPTOKEN_ISSUANCE)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey])
|
||||
{
|
||||
// This is enforced in preflight.
|
||||
XRPL_ASSERT(
|
||||
sle->getType() == ltMPTOKEN_ISSUANCE,
|
||||
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
// NOTE: presence must be checked before the key is overwritten below.
|
||||
bool const isRotation = sle->isFieldPresent(keyField);
|
||||
sle->setFieldVL(keyField, *pubKey);
|
||||
|
||||
sle->setFieldVL(sfAuditorEncryptionKey, *pubKey);
|
||||
}
|
||||
if (isRotation)
|
||||
{
|
||||
// Preclaim rejects overwriting an existing key unless the amendment is
|
||||
// enabled.
|
||||
if (!canRotateKey)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::MPTokenIssuanceSet::doApply : rotation without amendment");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const epoch = (*sle)[~epochField].valueOr(0);
|
||||
|
||||
// Preclaim rejects a rotation that would wrap the epoch. So this should never happen.
|
||||
if (epoch >= kMaxKeyEpoch)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::MPTokenIssuanceSet::doApply : key epoch overflow");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
(*sle)[epochField] = epoch + 1;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
};
|
||||
|
||||
if (auto const ter = setEncryptionKey(sfIssuerEncryptionKey, sfIssuerKeyEpoch);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ter = setEncryptionKey(sfAuditorEncryptionKey, sfAuditorKeyEpoch);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
|
||||
view().update(sle);
|
||||
|
||||
|
||||
634
src/test/app/ConfidentialMPTKeyRotation_test.cpp
Normal file
634
src/test/app/ConfidentialMPTKeyRotation_test.cpp
Normal file
@@ -0,0 +1,634 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/ConfidentialTransfer.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
|
||||
{
|
||||
void
|
||||
testMPTokenIssuanceSetRotateIssuerKey(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet rotate issuer key");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
|
||||
// First-time registration.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
// Verify that no epochs are set when registering for the first time.
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
|
||||
// Rotating the issuer key requires the key rotation amendment
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
|
||||
});
|
||||
|
||||
// A rotation replaces the issuer key and bumps its epoch. The auditor
|
||||
// key was never registered, so it and its epoch stay absent.
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
// A second rotation increments the epoch again
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, std::nullopt));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRotateBothKeys(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet rotate both issuer and auditor keys");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
// Register both keys together.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
// Verify that no epochs are set when registering for the first time.
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
|
||||
// Rotating both keys requires the amendment
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
.auditorPubKey = mptAlice.getPubKey(alice),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
|
||||
});
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, alice));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
// Rotating the issuer key to its current value fails.
|
||||
// Current issuer key is bob, duplicate.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
|
||||
// Rotating the auditor key to its current value fails.
|
||||
// Current auditor key is alice, duplicate.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(alice),
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
|
||||
// The whole transaction fails when one key is unchanged, even if
|
||||
// the other key is rotated to a new value.
|
||||
// Current issuer key is bob, duplicate.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
|
||||
// Current auditor key is alice, duplicate.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(auditor),
|
||||
.auditorPubKey = mptAlice.getPubKey(alice),
|
||||
.err = tecDUPLICATE,
|
||||
});
|
||||
|
||||
// Nothing changed: keys and epochs are untouched
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
|
||||
|
||||
// A second rotation increments both epochs again
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, 2u));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRotateAuditorKeyOnly(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet rotate auditor key only");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
// Register both keys together.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
// A transaction carrying only the auditor key fails preflight
|
||||
// pre-ConfidentialMPTKeyRotation; post-ConfidentialMPTKeyRotation it rotates the auditor
|
||||
// key
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
|
||||
});
|
||||
|
||||
// The issuer key keeps unchanged, and rotating only the auditor key
|
||||
// bumps only its epoch.
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, bob));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
// A second rotation increments the epoch again
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
// The issuer key epoch is still untouched.
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 2u));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLater(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet register auditor key after issuer key");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice);
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
// Register the issuer key first. We'll register the auditor key in a separate transaction.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
// Register the auditor key separately.
|
||||
// pre-ConfidentialMPTKeyRotation it fails preflight; post-ConfidentialMPTKeyRotation it
|
||||
// succeeds without touching any epoch because it's a first-time registration.
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(
|
||||
alice, rotationEnabled ? std::optional<Account>(auditor) : std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet register auditor key later with circulating supply");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
// Convert some of bob's balance so that COA > 0
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 50,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
auto const sleIssuanceBefore = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuanceBefore))
|
||||
return;
|
||||
auto const coaBefore = (*sleIssuanceBefore)[~sfConfidentialOutstandingAmount].value_or(0);
|
||||
BEAST_EXPECT(coaBefore > 0);
|
||||
|
||||
// Registering the auditor key for the first time while confidential
|
||||
// supply is circulating: pre-ConfidentialMPTKeyRotation an auditor-only
|
||||
// transaction fails preflight; post-ConfidentialMPTKeyRotation it
|
||||
// succeeds as a first-time late-registration even COA > 0.
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(
|
||||
alice, rotationEnabled ? std::optional<Account>(auditor) : std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
|
||||
// The circulating supply itself is not affected.
|
||||
BEAST_EXPECT((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBefore);
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet auditor key requires issuer key");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice);
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
// The issuer key was never registered. pre-ConfidentialMPTKeyRotation an auditor-only
|
||||
// transaction fails preflight; post-ConfidentialMPTKeyRotation it passes preflight
|
||||
// but preclaim rejects registering an auditor key on an issuance
|
||||
// without an issuer key.
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
.err = rotationEnabled ? TER(tecNO_PERMISSION) : TER(temMALFORMED),
|
||||
});
|
||||
|
||||
// The rejected transaction leaves the issuance without either key.
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(std::nullopt, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetRotateWithCOA(FeatureBitset features)
|
||||
{
|
||||
testcase("MPTokenIssuanceSet rotate with circulating confidential supply");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(carol);
|
||||
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
// Convert some of bob's balance to confidential spending, so that the
|
||||
// issuance has confidential supply. COA > 0.
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 50,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
auto const sleIssuanceBeforeRotation =
|
||||
env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuanceBeforeRotation))
|
||||
return;
|
||||
auto const coaBeforeRotation =
|
||||
(*sleIssuanceBeforeRotation)[~sfConfidentialOutstandingAmount].value_or(0);
|
||||
BEAST_EXPECT(coaBeforeRotation > 0);
|
||||
|
||||
// Rotating key requires the
|
||||
// amendment.
|
||||
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(carol),
|
||||
.err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(carol, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
}
|
||||
|
||||
// The confidential outstanding amount is not affected by the rotation
|
||||
BEAST_EXPECT(
|
||||
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBeforeRotation);
|
||||
|
||||
// Re-enabling confidential balances while supply is circulating is
|
||||
// rejected regardless of the ConfidentialMPTKeyRotation amendment.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.flags = tfMPTSetCanHoldConfidentialBalance,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetKeyEpochAtMax(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
if (!features[featureConfidentialMPTKeyRotation])
|
||||
return;
|
||||
|
||||
testcase("MPTokenIssuanceSet key epoch cannot wrap");
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
Account const auditor("auditor");
|
||||
|
||||
// Keep the ledger open so that we can write the key epochs directly into it.
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}, .close = false});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
|
||||
});
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(carol);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
auto const issuanceKeylet = keylet::mptokenIssuance(mptAlice.issuanceID());
|
||||
|
||||
// Writes the supplied key epochs straight into the open ledger so that
|
||||
// the maximum epoch is reachable without submitting four billion
|
||||
// rotations.
|
||||
auto setEpochs = [&](std::optional<std::uint32_t> const& issuerKeyEpoch,
|
||||
std::optional<std::uint32_t> const& auditorKeyEpoch) {
|
||||
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
|
||||
auto const sle = view.read(issuanceKeylet);
|
||||
if (!sle)
|
||||
return false; // LCOV_EXCL_LINE
|
||||
|
||||
auto replacement = std::make_shared<SLE>(*sle);
|
||||
if (issuerKeyEpoch)
|
||||
(*replacement)[sfIssuerKeyEpoch] = *issuerKeyEpoch;
|
||||
if (auditorKeyEpoch)
|
||||
(*replacement)[sfAuditorKeyEpoch] = *auditorKeyEpoch;
|
||||
view.rawReplace(replacement);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
|
||||
|
||||
// Increment the auditor epoch to kMaxKeyEpoch - 1, leaving the issuer epoch absent.
|
||||
setEpochs(std::nullopt, kMaxKeyEpoch - 1);
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch - 1));
|
||||
|
||||
// Rotating the auditor key to kMaxKeyEpoch succeeds.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(carol),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
|
||||
|
||||
// A further auditor rotation is rejected because the epoch is exhausted.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
|
||||
// Rotating both keys at once is rejected as a whole because the auditor
|
||||
// epoch is exhausted.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(auditor),
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
|
||||
// Both rejections leave every key and epoch as it was.
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
|
||||
|
||||
// The issuer key is unaffected by the exhausted auditor epoch.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, kMaxKeyEpoch));
|
||||
|
||||
// Increment the issuer epoch to kMaxKeyEpoch - 1.
|
||||
setEpochs(kMaxKeyEpoch - 1, std::nullopt);
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch - 1, kMaxKeyEpoch));
|
||||
|
||||
// Rotating the issuer key to kMaxKeyEpoch succeeds.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
|
||||
|
||||
// With both epochs exhausted neither key can be rotated again.
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
|
||||
BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
|
||||
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
|
||||
}
|
||||
|
||||
void
|
||||
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
|
||||
{
|
||||
testMPTokenIssuanceSetRotateIssuerKey(features);
|
||||
testMPTokenIssuanceSetRotateBothKeys(features);
|
||||
testMPTokenIssuanceSetRotateAuditorKeyOnly(features);
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLater(features);
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(features);
|
||||
testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(features);
|
||||
testMPTokenIssuanceSetRotateWithCOA(features);
|
||||
testMPTokenIssuanceSetKeyEpochAtMax(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
|
||||
testMPTokenIssuanceSetWithFeats(all);
|
||||
testMPTokenIssuanceSetWithFeats(all - featureConfidentialMPTKeyRotation);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ConfidentialMPTKeyRotation, app, xrpl);
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -736,12 +736,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
|
||||
// Cannot set auditor key without issuer key
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(alice),
|
||||
.err = temMALFORMED,
|
||||
});
|
||||
// Note: "auditor key without issuer key" (temMALFORMED before
|
||||
// ConfidentialMPTKeyRotation) is covered in ConfidentialMPTKeyRotation_test
|
||||
|
||||
// Cannot set Holder and issuer Keys in the same transaction
|
||||
mptAlice.set({
|
||||
@@ -787,9 +783,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
});
|
||||
}
|
||||
|
||||
// Cannot update issuer public key once set
|
||||
// Cannot update issuer public key once set (pre-ConfidentialMPTKeyRotation behavior)
|
||||
{
|
||||
Env env{*this, features};
|
||||
Env env{*this, features - featureConfidentialMPTKeyRotation};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
@@ -819,8 +815,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
// Cannot update issuer and auditor public keys once set
|
||||
// Note: trying to set only auditor key fails in preflight (temMALFORMED)
|
||||
// so we must provide both keys, which fails on issuer key check first
|
||||
// (pre-ConfidentialMPTKeyRotation behavior)
|
||||
{
|
||||
Env env{*this, features};
|
||||
Env env{*this, features - featureConfidentialMPTKeyRotation};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const auditor("auditor");
|
||||
@@ -900,8 +897,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
}
|
||||
|
||||
// Set issuer key first, then auditor key in a separate tx
|
||||
// (pre-ConfidentialMPTKeyRotation behavior)
|
||||
{
|
||||
Env env{*this, features};
|
||||
Env env{*this, features - featureConfidentialMPTKeyRotation};
|
||||
Account const alice("alice");
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
|
||||
|
||||
@@ -648,6 +648,38 @@ MPTTester::checkImmutableFlags(std::uint32_t expectedFlags) const
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::checkKeyEpochs(
|
||||
std::optional<std::uint32_t> issuerKeyEpoch,
|
||||
std::optional<std::uint32_t> auditorKeyEpoch) const
|
||||
{
|
||||
return forObject([&](SLEP const& sle) -> bool {
|
||||
return (*sle)[~sfIssuerKeyEpoch] == issuerKeyEpoch &&
|
||||
(*sle)[~sfAuditorKeyEpoch] == auditorKeyEpoch;
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::checkEncryptionKeys(
|
||||
std::optional<Account> const& issuerKeyOwner,
|
||||
std::optional<Account> const& auditorKeyOwner) const
|
||||
{
|
||||
auto const matches =
|
||||
[this](SLEP const& sle, SF_VL const& field, std::optional<Account> const& owner) {
|
||||
if (!owner)
|
||||
return !sle->isFieldPresent(field);
|
||||
|
||||
auto const expected = getPubKey(*owner);
|
||||
return expected && sle->isFieldPresent(field) &&
|
||||
strHex((*sle)[field]) == strHex(*expected);
|
||||
};
|
||||
|
||||
return forObject([&](SLEP const& sle) -> bool {
|
||||
return matches(sle, sfIssuerEncryptionKey, issuerKeyOwner) &&
|
||||
matches(sle, sfAuditorEncryptionKey, auditorKeyOwner);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::pay(
|
||||
Account const& src,
|
||||
|
||||
@@ -612,6 +612,21 @@ public:
|
||||
[[nodiscard]] bool
|
||||
checkImmutableFlags(std::uint32_t expectedFlags) const;
|
||||
|
||||
// Checks both key epochs on the issuance. Pass std::nullopt for an epoch
|
||||
// that is expected to be absent, which means the key is never rotated.
|
||||
[[nodiscard]] bool
|
||||
checkKeyEpochs(
|
||||
std::optional<std::uint32_t> issuerKeyEpoch,
|
||||
std::optional<std::uint32_t> auditorKeyEpoch) const;
|
||||
|
||||
// Checks that the issuance carries the encryption keys of the given
|
||||
// accounts. Pass std::nullopt for a key that is expected to be absent,
|
||||
// which means the key is never registered.
|
||||
[[nodiscard]] bool
|
||||
checkEncryptionKeys(
|
||||
std::optional<Account> const& issuerKeyOwner,
|
||||
std::optional<Account> const& auditorKeyOwner) const;
|
||||
|
||||
[[nodiscard]] Account const&
|
||||
issuer() const
|
||||
{
|
||||
|
||||
@@ -36,6 +36,8 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
|
||||
auto const referenceHoldingValue = canonical_UINT256();
|
||||
auto const issuerEncryptionKeyValue = canonical_VL();
|
||||
auto const auditorEncryptionKeyValue = canonical_VL();
|
||||
auto const issuerKeyEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyEpochValue = canonical_UINT32();
|
||||
auto const confidentialOutstandingAmountValue = canonical_UINT64();
|
||||
|
||||
MPTokenIssuanceBuilder builder{
|
||||
@@ -57,6 +59,8 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
|
||||
builder.setReferenceHolding(referenceHoldingValue);
|
||||
builder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
|
||||
builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
|
||||
builder.setIssuerKeyEpoch(issuerKeyEpochValue);
|
||||
builder.setAuditorKeyEpoch(auditorKeyEpochValue);
|
||||
builder.setConfidentialOutstandingAmount(confidentialOutstandingAmountValue);
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
@@ -184,6 +188,22 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasAuditorEncryptionKey());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerKeyEpochValue;
|
||||
auto const actualOpt = entry.getIssuerKeyEpoch();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfIssuerKeyEpoch");
|
||||
EXPECT_TRUE(entry.hasIssuerKeyEpoch());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = auditorKeyEpochValue;
|
||||
auto const actualOpt = entry.getAuditorKeyEpoch();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfAuditorKeyEpoch");
|
||||
EXPECT_TRUE(entry.hasAuditorKeyEpoch());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialOutstandingAmountValue;
|
||||
auto const actualOpt = entry.getConfidentialOutstandingAmount();
|
||||
@@ -221,6 +241,8 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
|
||||
auto const referenceHoldingValue = canonical_UINT256();
|
||||
auto const issuerEncryptionKeyValue = canonical_VL();
|
||||
auto const auditorEncryptionKeyValue = canonical_VL();
|
||||
auto const issuerKeyEpochValue = canonical_UINT32();
|
||||
auto const auditorKeyEpochValue = canonical_UINT32();
|
||||
auto const confidentialOutstandingAmountValue = canonical_UINT64();
|
||||
|
||||
auto sle = std::make_shared<SLE>(MPTokenIssuance::entryType, index);
|
||||
@@ -241,6 +263,8 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfReferenceHolding) = referenceHoldingValue;
|
||||
sle->at(sfIssuerEncryptionKey) = issuerEncryptionKeyValue;
|
||||
sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue;
|
||||
sle->at(sfIssuerKeyEpoch) = issuerKeyEpochValue;
|
||||
sle->at(sfAuditorKeyEpoch) = auditorKeyEpochValue;
|
||||
sle->at(sfConfidentialOutstandingAmount) = confidentialOutstandingAmountValue;
|
||||
|
||||
MPTokenIssuanceBuilder builderFromSle{sle};
|
||||
@@ -442,6 +466,32 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptionKey");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerKeyEpochValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getIssuerKeyEpoch();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getIssuerKeyEpoch();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfIssuerKeyEpoch");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfIssuerKeyEpoch");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = auditorKeyEpochValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getAuditorKeyEpoch();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getAuditorKeyEpoch();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfAuditorKeyEpoch");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfAuditorKeyEpoch");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = confidentialOutstandingAmountValue;
|
||||
|
||||
@@ -539,6 +589,10 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getIssuerEncryptionKey().has_value());
|
||||
EXPECT_FALSE(entry.hasAuditorEncryptionKey());
|
||||
EXPECT_FALSE(entry.getAuditorEncryptionKey().has_value());
|
||||
EXPECT_FALSE(entry.hasIssuerKeyEpoch());
|
||||
EXPECT_FALSE(entry.getIssuerKeyEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasAuditorKeyEpoch());
|
||||
EXPECT_FALSE(entry.getAuditorKeyEpoch().has_value());
|
||||
EXPECT_FALSE(entry.hasConfidentialOutstandingAmount());
|
||||
EXPECT_FALSE(entry.getConfidentialOutstandingAmount().has_value());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user