mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 00:36:48 +00:00
add public MPT txn after Clearing Confidential Flag (#7113)
This commit is contained in:
@@ -65,8 +65,8 @@ public:
|
||||
* Cannot delete if sfIssuerEncryptedBalance exists
|
||||
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
|
||||
* - Privacy flag consistency:
|
||||
* MPToken can only have encrypted fields if lsfMPTCanConfidentialAmount is set on
|
||||
* issuance.
|
||||
* MPToken confidential balance fields can only be created or changed if
|
||||
* lsfMPTCanConfidentialAmount is set on the issuance.
|
||||
* - Encrypted field existence consistency:
|
||||
* If sfConfidentialBalanceSpending/sfConfidentialBalanceInbox exists, then
|
||||
* sfIssuerEncryptedBalance must also exist (and vice versa).
|
||||
@@ -86,7 +86,7 @@ class ValidConfidentialMPToken
|
||||
bool deletedWithEncrypted = false;
|
||||
bool badConsistency = false;
|
||||
bool badCOA = false;
|
||||
bool requiresPrivacyFlag = false;
|
||||
bool changesConfidentialFields = false;
|
||||
bool badVersion = false;
|
||||
};
|
||||
std::map<uint192, Changes> changes_;
|
||||
|
||||
@@ -450,17 +450,29 @@ ValidConfidentialMPToken::visitEntry(
|
||||
bool const hasHolderInbox = after->isFieldPresent(sfConfidentialBalanceInbox);
|
||||
bool const hasHolderSpending = after->isFieldPresent(sfConfidentialBalanceSpending);
|
||||
|
||||
bool const hasAnyHolder = hasHolderInbox || hasHolderSpending;
|
||||
|
||||
// sfIssuerEncryptedBalance, sfConfidentialBalanceInbox, and sfConfidentialBalanceSpending
|
||||
// must all exist or not exist same time.
|
||||
if (hasHolderInbox != hasHolderSpending || hasHolderInbox != hasIssuerBalance)
|
||||
changes_[id].badConsistency = true;
|
||||
|
||||
// Privacy flag consistency
|
||||
bool const hasEncrypted = hasAnyHolder || hasIssuerBalance;
|
||||
if (hasEncrypted)
|
||||
changes_[id].requiresPrivacyFlag = true;
|
||||
auto const confidentialBalanceFieldChanged = [&before, &after](auto const& field) {
|
||||
auto const afterValue = (*after)[~field];
|
||||
if (!afterValue)
|
||||
return false;
|
||||
|
||||
if (!before || before->getType() != ltMPTOKEN)
|
||||
return true; // LCOV_EXCL_LINE
|
||||
|
||||
return (*before)[~field] != afterValue;
|
||||
};
|
||||
|
||||
if (confidentialBalanceFieldChanged(sfConfidentialBalanceInbox) ||
|
||||
confidentialBalanceFieldChanged(sfConfidentialBalanceSpending) ||
|
||||
confidentialBalanceFieldChanged(sfIssuerEncryptedBalance) ||
|
||||
confidentialBalanceFieldChanged(sfAuditorEncryptedBalance))
|
||||
{
|
||||
changes_[id].changesConfidentialFields = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (before && before->getType() == ltMPTOKEN_ISSUANCE)
|
||||
@@ -563,8 +575,10 @@ ValidConfidentialMPToken::finalize(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Privacy flag consistency
|
||||
if (checks.requiresPrivacyFlag)
|
||||
// Confidential balance fields may remain on a holder MPToken after all
|
||||
// confidential balances have returned to zero. Only creating or
|
||||
// changing those fields requires the issuance privacy flag.
|
||||
if (checks.changesConfidentialFields)
|
||||
{
|
||||
if (!issuance->isFlag(lsfMPTCanConfidentialAmount))
|
||||
{
|
||||
@@ -600,6 +614,16 @@ ValidConfidentialMPToken::finalize(
|
||||
std::ranges::find(kCONFIDENTIAL_MPT_TX_TYPES, tx.getTxnType()) !=
|
||||
kCONFIDENTIAL_MPT_TX_TYPES.end())
|
||||
{
|
||||
// Confidential Txns should not modify public MPTAmount balance
|
||||
// if Confidential Amount Delta is 0
|
||||
if (checks.mptAmountDelta != 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPTAmount changed by confidential "
|
||||
"transaction that should not modify this field."
|
||||
<< to_string(id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Among confidential MPT transactions, only ConfidentialMPTSend and
|
||||
// ConfidentialMPTMergeInbox leave coaDelta unmodified. Therefore, if a confidential MPT
|
||||
// transaction reaches here, it must be one of these two types, neither of which will
|
||||
|
||||
@@ -5325,6 +5325,126 @@ class ConfidentialTransfer_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPublicTransfersAfterClearingConfidentialFlag(FeatureBitset features)
|
||||
{
|
||||
testcase("Public transfers after clearing Confidential Flag");
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
|
||||
// After clearing the confidential flag, all four public MPT operations
|
||||
// must succeed regardless of which confidential path left encrypted-zero
|
||||
// fields on bob's MPToken.
|
||||
auto runPublicPayments = [&](MPTTester& mpt) {
|
||||
mpt.pay(bob, carol, 10);
|
||||
mpt.pay(carol, bob, 5);
|
||||
mpt.pay(alice, bob, 1);
|
||||
mpt.pay(carol, alice, 5);
|
||||
};
|
||||
|
||||
auto drainAndDeleteBobMPToken = [&](Env& env, MPTTester& mpt) {
|
||||
auto const bobBalance = mpt.getBalance(bob);
|
||||
BEAST_EXPECT(bobBalance > 0);
|
||||
|
||||
mpt.pay(bob, alice, bobBalance);
|
||||
BEAST_EXPECT(mpt.getBalance(bob) == 0);
|
||||
|
||||
mpt.authorize({.account = bob, .flags = tfMPTUnauthorize});
|
||||
BEAST_EXPECT(!env.le(keylet::mptoken(mpt.issuanceID(), bob.id())));
|
||||
};
|
||||
|
||||
// Alice pays Bob 100 public, Bob converts 50 confidential
|
||||
// Bob converts 50 back to public, and make sure can receive public payments
|
||||
{
|
||||
Env env{*this, features};
|
||||
ConfidentialEnv ct{
|
||||
env,
|
||||
alice,
|
||||
{{.account = bob, .payAmount = 100, .convertAmount = 50}},
|
||||
tfMPTCanTransfer | tfMPTCanConfidentialAmount};
|
||||
|
||||
env.fund(XRP(1'000), carol);
|
||||
ct.mpt.authorize({.account = carol});
|
||||
ct.mpt.pay(alice, carol, 50);
|
||||
|
||||
ct.mpt.convertBack({.account = bob, .amt = 50});
|
||||
ct.mpt.set({
|
||||
.account = alice,
|
||||
.mutableFlags = tmfMPTClearCanConfidentialAmount,
|
||||
});
|
||||
|
||||
runPublicPayments(ct.mpt);
|
||||
drainAndDeleteBobMPToken(env, ct.mpt);
|
||||
}
|
||||
|
||||
// Same path as above but with Auditor
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const auditor("auditor");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanConfidentialAmount,
|
||||
});
|
||||
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = carol});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
mptAlice.pay(alice, carol, 50);
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(auditor);
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.issuerPubKey = mptAlice.getPubKey(alice),
|
||||
.auditorPubKey = mptAlice.getPubKey(auditor)});
|
||||
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 50,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
mptAlice.mergeInbox({.account = bob});
|
||||
mptAlice.convertBack({.account = bob, .amt = 50});
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.mutableFlags = tmfMPTClearCanConfidentialAmount,
|
||||
});
|
||||
|
||||
runPublicPayments(mptAlice);
|
||||
drainAndDeleteBobMPToken(env, mptAlice);
|
||||
}
|
||||
|
||||
// Confidential clawback leaves encrypted-zero fields;
|
||||
// the public balance remaining after the clawback must stay usable.
|
||||
{
|
||||
Env env{*this, features};
|
||||
ConfidentialEnv ct{
|
||||
env,
|
||||
alice,
|
||||
{{.account = bob, .payAmount = 100, .convertAmount = 50}},
|
||||
tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanConfidentialAmount};
|
||||
|
||||
env.fund(XRP(1'000), carol);
|
||||
ct.mpt.authorize({.account = carol});
|
||||
ct.mpt.pay(alice, carol, 50);
|
||||
|
||||
ct.mpt.confidentialClaw({.account = alice, .holder = bob, .amt = 50});
|
||||
ct.mpt.set({
|
||||
.account = alice,
|
||||
.mutableFlags = tmfMPTClearCanConfidentialAmount,
|
||||
});
|
||||
|
||||
runPublicPayments(ct.mpt);
|
||||
drainAndDeleteBobMPToken(env, ct.mpt);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMutatePrivacy(FeatureBitset features)
|
||||
{
|
||||
@@ -10114,10 +10234,11 @@ class ConfidentialTransfer_test : public beast::unit_test::Suite
|
||||
testIdentityElementRejection(features);
|
||||
testSendWrongIssuerPublicKey(features);
|
||||
|
||||
// Replay Tests
|
||||
testMutatePrivacy(features);
|
||||
// public and private txns
|
||||
testPublicTransfersAfterClearingConfidentialFlag(features);
|
||||
|
||||
// Replay tests
|
||||
testMutatePrivacy(features);
|
||||
testProofContextBinding(features);
|
||||
testProofCiphertextBinding(features);
|
||||
testProofVersionMismatch(features);
|
||||
|
||||
@@ -4515,6 +4515,24 @@ class Invariants_test : public beast::unit_test::Suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseConfidential);
|
||||
|
||||
// Send/MergeInbox and zero-COA-delta confidential transactions must not
|
||||
// change public holder MPTAmount.
|
||||
doInvariantCheck(
|
||||
{"Invariant failed: MPTAmount changed by confidential "
|
||||
"transaction that should not modify this field."},
|
||||
[&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
|
||||
if (!sleToken)
|
||||
return false;
|
||||
sleToken->setFieldU64(sfMPTAmount, sleToken->getFieldU64(sfMPTAmount) + 1);
|
||||
ac.view().update(sleToken);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseConfidential);
|
||||
|
||||
// badVersion
|
||||
doInvariantCheck(
|
||||
{"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending "
|
||||
|
||||
Reference in New Issue
Block a user