Remove lsfMPTDomainCheck flag, delete expired MPToken

This commit is contained in:
Bronek Kozicki
2025-03-19 11:51:03 +00:00
parent 2a8861d1c5
commit 8e68838543
6 changed files with 104 additions and 93 deletions

View File

@@ -188,7 +188,6 @@ enum LedgerSpecificFlags {
// ltMPTOKEN // ltMPTOKEN
lsfMPTAuthorized = 0x00000002, lsfMPTAuthorized = 0x00000002,
lsfMPTDomainCheck = 0x00000004,
// ltCREDENTIAL // ltCREDENTIAL
lsfAccepted = 0x00010000, lsfAccepted = 0x00010000,

View File

@@ -749,6 +749,16 @@ class Vault_test : public beast::unit_test::suite
.amount = asset(100)}); .amount = asset(100)});
env(tx); env(tx);
} }
{
testcase("private vault depositor not authorized");
auto tx = vault.deposit(
{.depositor = depositor,
.id = keylet.key,
.amount = asset(50)});
env(tx, ter{tecNO_AUTH});
env.close();
}
} }
public: public:

View File

@@ -78,19 +78,12 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
// would allow authorization granted by the issuer explicitly, but Vault // would allow authorization granted by the issuer explicitly, but Vault
// does not have an MPT issuer (instead it uses pseudo-account, which is // does not have an MPT issuer (instead it uses pseudo-account, which is
// blackholed and cannot create any transactions). // blackholed and cannot create any transactions).
//
// We also need to do similar check inside doApply(), in order to remove
// expired credentials and/or adjust authorization flag on tokens owned
// by DomainID (i.e. with lsfMPTDomainCheck flag). This is why we
// suppress authorization errors if domainId is set.
uint256 domainId = beast::zero;
auto const err = requireAuth( auto const err = requireAuth(
ctx.view, ctx.view, MPTIssue(vault->at(sfMPTokenIssuanceID)), account);
MPTIssue(vault->at(sfMPTokenIssuanceID)),
account,
&domainId);
if (domainId == beast::zero) // As per requireAuth spec, we suppress tecEXPIRED error here, so we can
// delete any expired credentials inside doApply.
if (err != tecEXPIRED)
return err; return err;
} }

View File

@@ -64,19 +64,33 @@ TER
VaultSet::preclaim(PreclaimContext const& ctx) VaultSet::preclaim(PreclaimContext const& ctx)
{ {
auto const id = ctx.tx[sfVaultID]; auto const id = ctx.tx[sfVaultID];
auto const sle = ctx.view.read(keylet::vault(id)); auto const vault = ctx.view.read(keylet::vault(id));
if (!sle) if (!vault)
return tecOBJECT_NOT_FOUND; return tecOBJECT_NOT_FOUND;
// Assert that submitter is the Owner. // Assert that submitter is the Owner.
if (ctx.tx[sfAccount] != sle->at(sfOwner)) if (ctx.tx[sfAccount] != vault->at(sfOwner))
return tecNO_PERMISSION; return tecNO_PERMISSION;
// We can only set domain if private flag was originally set // We can only set domain if private flag was originally set
if (auto const domain = ctx.tx[~sfDomainID]) if (auto const domain = ctx.tx[~sfDomainID])
{ {
if ((sle->getFlags() & tfVaultPrivate) == 0) if ((vault->getFlags() & tfVaultPrivate) == 0)
return tecINVALID_DOMAIN; return tecINVALID_DOMAIN;
auto const sleDomain =
ctx.view.read(keylet::permissionedDomain(*domain));
if (!sleDomain)
return tecNO_ENTRY;
// Sanity checks, all this should be enforced by VaultCreate
auto const mptIssuanceID = (*vault)[sfMPTokenIssuanceID];
auto const sleIssuance =
ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tefINTERNAL;
if ((sleIssuance->getFlags() & lsfMPTRequireAuth) == 0)
return tefINTERNAL;
} }
return tesSUCCESS; return tesSUCCESS;

View File

@@ -647,22 +647,22 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account);
* *
* This will also check for expired credentials. If it is called directly * This will also check for expired credentials. If it is called directly
* from preclaim, the user should convert result tecEXPIRED to tesSUCCESS and * from preclaim, the user should convert result tecEXPIRED to tesSUCCESS and
* proceed to also check permissions with verifyValidDomain inside doApply. * proceed to also check permissions with enforceMPTokenAuthorization inside
* This will ensure that any expired credentials are deleted. * doApply. This will ensure that any expired credentials are deleted.
*/ */
[[nodiscard]] TER [[nodiscard]] TER
requireAuth( requireAuth(
ReadView const& view, ReadView const& view,
MPTIssue const& mptIssue, MPTIssue const& mptIssue,
AccountID const& account, AccountID const& account);
uint256* domainId = nullptr);
/** Check if the account lacks required authorization. /** Enforce account has MPToken to match its authorization.
* *
* Called from doApply - it will check for expired (and delete if found any) * Called from doApply - it will check for expired (and delete if found any)
* credentials maching DomainID set in MPTIssuance. Must be called if * credentials maching DomainID set in MPTIssuance. Must be called if
* requireAuth(...MPTIssue...) check passed in preclaim. Will create MPToken * requireAuth(...MPTIssue...) returned tesSUCCESS or tecEXPIRED in preclaim.
* if needed, on the basis of non-expired credentals found. * Will create MPToken (if needed) on the basis of any non-expired credentals
* and delete any expried credentials (indirectly via verifyValidDomain)
*/ */
[[nodiscard]] TER [[nodiscard]] TER
enforceMPTokenAuthorization( enforceMPTokenAuthorization(

View File

@@ -2183,8 +2183,7 @@ TER
requireAuth( requireAuth(
ReadView const& view, ReadView const& view,
MPTIssue const& mptIssue, MPTIssue const& mptIssue,
AccountID const& account, AccountID const& account)
uint256* domainId)
{ {
auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); auto const mptID = keylet::mptIssuance(mptIssue.getMptID());
auto const sleIssuance = view.read(mptID); auto const sleIssuance = view.read(mptID);
@@ -2222,10 +2221,12 @@ requireAuth(
credentials::validDomain(view, *maybeDomainID, account); credentials::validDomain(view, *maybeDomainID, account);
!isTesSuccess(err)) !isTesSuccess(err))
{ {
if (err != tecINVALID_DOMAIN && domainId != nullptr) // Force tecEXPIRED error if there's an sleToken but no authorization -
(*domainId) = *maybeDomainID; // i.e. an expired MPToken which should be deleted (if zero balance)
if (sleToken)
return err; return tecEXPIRED;
else
return err;
} }
// We are authorized by permissioned domain. // We are authorized by permissioned domain.
@@ -2247,86 +2248,86 @@ enforceMPTokenAuthorization(
XRPL_ASSERT( XRPL_ASSERT(
sleIssuance->getFlags() & lsfMPTRequireAuth, sleIssuance->getFlags() & lsfMPTRequireAuth,
"ripple::verifyAuth : MPTokenIssuance requires authorization"); "ripple::enforceMPTokenAuthorization : authorization required");
if (account == sleIssuance->at(sfIssuer)) if (account == sleIssuance->at(sfIssuer))
return tesSUCCESS; // Won't create MPToken for the token issuer return tesSUCCESS; // Won't create MPToken for the token issuer
auto const keylet = keylet::mptoken(mptIssuanceID, account); auto const keylet = keylet::mptoken(mptIssuanceID, account);
auto sleToken = view.read(keylet); auto const sleToken = view.read(keylet); // NOTE: might be null
bool const domainOwned = auto const maybeDomainID = sleIssuance->at(~sfDomainID);
(sleToken && (sleToken->getFlags() & lsfMPTDomainCheck)); bool const authorizedByDomain = maybeDomainID.has_value() &&
verifyValidDomain(view, account, *maybeDomainID, j) == tesSUCCESS;
bool authorizedByDomain = false;
if (domainOwned || sleToken == nullptr)
{
// We check DomainID if:
//
// 1. Token not found or
// 2. Token found and has lsfMPTDomainCheck flag
//
// In case 1. we check authorization in order to create the token
// (below), but only if the account is authorized by the domain. In
// case 2. we re-check authorization in case the credentials are
// expired, in which case the token needs to be unauthorized (below)
auto const maybeDomainID = sleIssuance->at(~sfDomainID);
authorizedByDomain = maybeDomainID.has_value() &&
verifyValidDomain(view, account, *maybeDomainID, j) == tesSUCCESS;
}
if (!authorizedByDomain && sleToken == nullptr) if (!authorizedByDomain && sleToken == nullptr)
{ {
// Intentionally empty. This could be either of: // Could not find MPToken and won't create one, could be either of:
// //
// 1. Field sfDomainID not set in MPTokenIssuance or // 1. Field sfDomainID not set in MPTokenIssuance or
// 2. Account has no matching and accepted credentials or // 2. Account has no matching and accepted credentials or
// 3. Account has all expired credentials (removed in verifyValidDomain) // 3. Account has all expired credentials (deleted in verifyValidDomain)
// //
// Either way, will return tecNO_AUTH at the end of this function // Either way, return tecNO_AUTH and there is nothing else to do
return tecNO_AUTH;
} }
else if (!authorizedByDomain && domainOwned) else if (!authorizedByDomain && maybeDomainID.has_value())
{ {
// We found an MPToken with lsfMPTDomainCheck flag, but the account is // Found an MPToken but the account is not authorized and we expect
// no longer authorized. // it to have been authorized by the domain. This could be because the
if (sleToken->getFlags() & lsfMPTAuthorized) // credentials used to create the MPToken have expired or been deleted.
{ //
// Must reset lsfMPTAuthorized, no current credentials // Delete the expired MPToken but only if it has zero balance, then
auto sleMpt = view.peek(keylet); // return tecNO_AUTH
XRPL_ASSERT(sleMpt, "ripple::verifyAuth : non-null bad MPToken");
sleMpt->clearFlag(lsfMPTAuthorized);
view.update(sleMpt);
sleToken = sleMpt; // without lsfMPTAuthorized auto sleMpt = view.peek(keylet);
auto sleAcct = view.peek(keylet::account(account));
XRPL_ASSERT(
sleMpt != nullptr && sleAcct != nullptr,
"ripple::enforceMPTokenAuthorization : found expired MPToken");
if ((*sleMpt)[sfMPTAmount] == 0)
{
if (!view.dirRemove(
keylet::ownerDir(account),
(*sleMpt)[sfOwnerNode],
sleMpt->key(),
false))
return tecINTERNAL;
adjustOwnerCount(view, sleAcct, -1, j);
view.erase(sleMpt);
} }
return tecNO_AUTH;
} }
else if (!authorizedByDomain) else if (!authorizedByDomain)
{ {
// We found an MPToken, but sfDomainID is not set, so this is a classic
// MPToken which requires authorization by the token issuer.
XRPL_ASSERT( XRPL_ASSERT(
sleToken != nullptr && !domainOwned, sleToken != nullptr && !maybeDomainID.has_value(),
"ripple::verifyAuth : MPToken not owned by domain"); "ripple::enforceMPTokenAuthorization : found MPToken");
// MPToken was created by other means, we will check its authorization if (sleToken->getFlags() & lsfMPTAuthorized)
// at the end of this function. No need to do anything here. return tesSUCCESS;
return tecNO_AUTH;
} }
else if (authorizedByDomain && sleToken != nullptr) else if (authorizedByDomain && sleToken != nullptr)
{ {
// Found an MPToken, authorized by the domain. Ignore authorization flag
// lsfMPTAuthorized because it is meaningless. Return tesSUCCESS
XRPL_ASSERT( XRPL_ASSERT(
domainOwned, "ripple::verifyAuth : MPToken owned by domain"); maybeDomainID.has_value(),
"ripple::enforceMPTokenAuthorization : found MPToken for domain");
if ((sleToken->getFlags() & lsfMPTAuthorized) == 0) return tesSUCCESS;
{
// Must set lsfMPTAuthorized, we found new credentials
auto sleMpt = view.peek(keylet);
XRPL_ASSERT(sleMpt, "ripple::verifyAuth : non-null good MPToken");
sleMpt->setFlag(lsfMPTAuthorized);
view.update(sleMpt);
sleToken = sleMpt; // with lsfMPTAuthorized
}
} }
else if (authorizedByDomain && sleToken == nullptr) else if (authorizedByDomain)
{ {
// Create MPToken with the lsfMPTDomainCheck flag set // Could not find MPToken but there should be one because we are
// authorized by domain. Proceed to create it, then return tesSUCCESS
XRPL_ASSERT(
maybeDomainID.has_value() && sleToken == nullptr,
"ripple::enforceMPTokenAuthorization : new MPToken for domain");
if (auto const err = MPTokenAuthorize::authorize( if (auto const err = MPTokenAuthorize::authorize(
view, view,
j, j,
@@ -2339,18 +2340,12 @@ enforceMPTokenAuthorization(
!isTesSuccess(err)) !isTesSuccess(err))
return err; return err;
auto sleMpt = view.peek(keylet); return tesSUCCESS;
XRPL_ASSERT(sleMpt, "ripple::verifyAuth : non-null new MPToken");
sleMpt->setFlag(lsfMPTDomainCheck | lsfMPTAuthorized);
view.update(sleMpt);
sleToken = sleMpt; // with lsfMPTAuthorized
} }
if (sleToken == nullptr || (sleToken->getFlags() & lsfMPTAuthorized) == 0) UNREACHABLE(
return tecNO_AUTH; "ripple::enforceMPTokenAuthorization : condition list is incomplete");
return tefINTERNAL;
return tesSUCCESS;
} }
TER TER