mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 23:30:54 +00:00
Compare commits
5 Commits
pratik/std
...
ripple/con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e680d64d66 | ||
|
|
16fbb6ffa5 | ||
|
|
243c16aa06 | ||
|
|
7e5aa1f984 | ||
|
|
8e54590852 |
@@ -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.
|
||||
|
||||
@@ -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,45 @@ 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;
|
||||
}
|
||||
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 +276,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 +417,32 @@ MPTokenIssuanceSet::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const pubKey = ctx_.tx[~sfIssuerEncryptionKey])
|
||||
{
|
||||
// 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.
|
||||
auto const setEncryptionKey = [&](SF_VL const& keyField, SF_UINT32 const& epochField) {
|
||||
auto const pubKey = ctx_.tx[~keyField];
|
||||
if (!pubKey)
|
||||
return;
|
||||
|
||||
// This is enforced in preflight.
|
||||
XRPL_ASSERT(
|
||||
sle->getType() == ltMPTOKEN_ISSUANCE,
|
||||
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
|
||||
sle->setFieldVL(sfIssuerEncryptionKey, *pubKey);
|
||||
}
|
||||
// NOTE: presence must be checked before the key is overwritten below.
|
||||
bool const isRotation = sle->isFieldPresent(keyField);
|
||||
|
||||
if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey])
|
||||
{
|
||||
// This is enforced in preflight.
|
||||
XRPL_ASSERT(
|
||||
sle->getType() == ltMPTOKEN_ISSUANCE,
|
||||
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
sle->setFieldVL(keyField, *pubKey);
|
||||
|
||||
sle->setFieldVL(sfAuditorEncryptionKey, *pubKey);
|
||||
}
|
||||
if (isRotation)
|
||||
(*sle)[epochField] = (*sle)[~epochField].valueOr(0) + 1;
|
||||
};
|
||||
|
||||
setEncryptionKey(sfIssuerEncryptionKey, sfIssuerKeyEpoch);
|
||||
setEncryptionKey(sfAuditorEncryptionKey, sfAuditorKeyEpoch);
|
||||
|
||||
view().update(sle);
|
||||
|
||||
|
||||
548
src/test/app/ConfidentialMPTKeyRotation_test.cpp
Normal file
548
src/test/app/ConfidentialMPTKeyRotation_test.cpp
Normal file
@@ -0,0 +1,548 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/ConfidentialTransfer.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
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.
|
||||
{
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
BEAST_EXPECT(sleIssuance);
|
||||
BEAST_EXPECT(sleIssuance && !sleIssuance->isFieldPresent(sfIssuerKeyEpoch));
|
||||
BEAST_EXPECT(sleIssuance && !sleIssuance->isFieldPresent(sfAuditorKeyEpoch));
|
||||
}
|
||||
|
||||
// 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),
|
||||
});
|
||||
|
||||
{
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
|
||||
auto const expectedKey =
|
||||
rotationEnabled ? mptAlice.getPubKey(bob) : mptAlice.getPubKey(alice);
|
||||
BEAST_EXPECT(
|
||||
expectedKey &&
|
||||
strHex((*sleIssuance)[sfIssuerEncryptionKey]) == strHex(*expectedKey));
|
||||
|
||||
// Rotating the issuer key bumps the epoch.
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT((*sleIssuance)[~sfIssuerKeyEpoch] == 1u);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfIssuerKeyEpoch));
|
||||
}
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfAuditorKeyEpoch));
|
||||
}
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
// A second rotation increments the epoch again
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfIssuerKeyEpoch] == 2u);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
{
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
BEAST_EXPECT(sleIssuance && !sleIssuance->isFieldPresent(sfIssuerKeyEpoch));
|
||||
BEAST_EXPECT(sleIssuance && !sleIssuance->isFieldPresent(sfAuditorKeyEpoch));
|
||||
}
|
||||
|
||||
// Rotating both keys, it 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),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
|
||||
auto const expectedIssuerKey =
|
||||
rotationEnabled ? mptAlice.getPubKey(bob) : mptAlice.getPubKey(alice);
|
||||
auto const expectedAuditorKey =
|
||||
rotationEnabled ? mptAlice.getPubKey(alice) : mptAlice.getPubKey(auditor);
|
||||
BEAST_EXPECT(
|
||||
expectedIssuerKey &&
|
||||
strHex((*sleIssuance)[sfIssuerEncryptionKey]) == strHex(*expectedIssuerKey));
|
||||
BEAST_EXPECT(
|
||||
expectedAuditorKey &&
|
||||
strHex((*sleIssuance)[sfAuditorEncryptionKey]) == strHex(*expectedAuditorKey));
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT((*sleIssuance)[~sfIssuerKeyEpoch] == 1u);
|
||||
BEAST_EXPECT((*sleIssuance)[~sfAuditorKeyEpoch] == 1u);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfIssuerKeyEpoch));
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfAuditorKeyEpoch));
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfIssuerKeyEpoch] == 1u);
|
||||
BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfAuditorKeyEpoch] == 1u);
|
||||
}
|
||||
|
||||
// A second rotation increments both epochs again
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfIssuerKeyEpoch] == 2u);
|
||||
BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfAuditorKeyEpoch] == 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),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
|
||||
// The issuer key keeps unchanged.
|
||||
auto const issuerKey = mptAlice.getPubKey(alice);
|
||||
BEAST_EXPECT(
|
||||
issuerKey && strHex((*sleIssuance)[sfIssuerEncryptionKey]) == strHex(*issuerKey));
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfIssuerKeyEpoch));
|
||||
|
||||
auto const expectedAuditorKey =
|
||||
rotationEnabled ? mptAlice.getPubKey(bob) : mptAlice.getPubKey(auditor);
|
||||
BEAST_EXPECT(
|
||||
expectedAuditorKey &&
|
||||
strHex((*sleIssuance)[sfAuditorEncryptionKey]) == strHex(*expectedAuditorKey));
|
||||
|
||||
// Rotating the auditor key bumps its epoch.
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT((*sleIssuance)[~sfAuditorKeyEpoch] == 1u);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfAuditorKeyEpoch));
|
||||
}
|
||||
|
||||
if (rotationEnabled)
|
||||
{
|
||||
// A second rotation increments the epoch again
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfAuditorKeyEpoch] == 2u);
|
||||
|
||||
// The issuer key epoch is still untouched.
|
||||
BEAST_EXPECT(sleIssuance && !sleIssuance->isFieldPresent(sfIssuerKeyEpoch));
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleIssuance))
|
||||
return;
|
||||
BEAST_EXPECT(sleIssuance->isFieldPresent(sfAuditorEncryptionKey) == rotationEnabled);
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfIssuerKeyEpoch));
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfAuditorKeyEpoch));
|
||||
}
|
||||
|
||||
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(sleIssuance->isFieldPresent(sfAuditorEncryptionKey) == rotationEnabled);
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfIssuerKeyEpoch));
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfAuditorKeyEpoch));
|
||||
|
||||
// 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),
|
||||
});
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
|
||||
BEAST_EXPECT(sleIssuance && !sleIssuance->isFieldPresent(sfAuditorEncryptionKey));
|
||||
}
|
||||
|
||||
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;
|
||||
auto const expectedKey =
|
||||
rotationEnabled ? mptAlice.getPubKey(carol) : mptAlice.getPubKey(alice);
|
||||
BEAST_EXPECT(
|
||||
expectedKey && strHex((*sleIssuance)[sfIssuerEncryptionKey]) == strHex(*expectedKey));
|
||||
if (rotationEnabled)
|
||||
{
|
||||
BEAST_EXPECT((*sleIssuance)[~sfIssuerKeyEpoch] == 1u);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!sleIssuance->isFieldPresent(sfIssuerKeyEpoch));
|
||||
}
|
||||
|
||||
// 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
|
||||
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
|
||||
{
|
||||
testMPTokenIssuanceSetRotateIssuerKey(features);
|
||||
testMPTokenIssuanceSetRotateBothKeys(features);
|
||||
testMPTokenIssuanceSetRotateAuditorKeyOnly(features);
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLater(features);
|
||||
testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(features);
|
||||
testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(features);
|
||||
testMPTokenIssuanceSetRotateWithCOA(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});
|
||||
|
||||
@@ -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