mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-07 10:47:05 +00:00
441 lines
13 KiB
C++
441 lines
13 KiB
C++
#include <xrpl/tx/transactors/account/AccountDelete.h>
|
|
|
|
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/basics/safe_cast.h>
|
|
#include <xrpl/beast/utility/Zero.h>
|
|
#include <xrpl/beast/utility/instrumentation.h>
|
|
#include <xrpl/core/ServiceRegistry.h>
|
|
#include <xrpl/ledger/ApplyView.h>
|
|
#include <xrpl/ledger/ReadView.h>
|
|
#include <xrpl/ledger/View.h>
|
|
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
|
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
|
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
|
|
#include <xrpl/ledger/helpers/OfferHelpers.h>
|
|
#include <xrpl/protocol/AccountID.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/Indexes.h>
|
|
#include <xrpl/protocol/Keylet.h>
|
|
#include <xrpl/protocol/LedgerFormats.h>
|
|
#include <xrpl/protocol/Protocol.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/STLedgerEntry.h>
|
|
#include <xrpl/protocol/STTx.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/XRPAmount.h>
|
|
#include <xrpl/tx/Transactor.h>
|
|
#include <xrpl/tx/transactors/account/SignerListSet.h>
|
|
#include <xrpl/tx/transactors/delegate/DelegateSet.h>
|
|
#include <xrpl/tx/transactors/did/DIDDelete.h>
|
|
#include <xrpl/tx/transactors/oracle/OracleDelete.h>
|
|
#include <xrpl/tx/transactors/payment/DepositPreauth.h>
|
|
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <utility>
|
|
|
|
namespace xrpl {
|
|
|
|
bool
|
|
AccountDelete::checkExtraFeatures(PreflightContext const& ctx)
|
|
{
|
|
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
|
|
}
|
|
|
|
NotTEC
|
|
AccountDelete::preflight(PreflightContext const& ctx)
|
|
{
|
|
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
|
|
{
|
|
// An account cannot be deleted and give itself the resulting XRP.
|
|
return temDST_IS_SRC;
|
|
}
|
|
|
|
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
|
|
return err;
|
|
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
XRPAmount
|
|
AccountDelete::calculateBaseFee(ReadView const& view, STTx const& tx)
|
|
{
|
|
// The fee required for AccountDelete is one owner reserve.
|
|
return calculateOwnerReserveFee(view, tx);
|
|
}
|
|
|
|
namespace {
|
|
// Define a function pointer type that can be used to delete ledger node types.
|
|
using DeleterFuncPtr = TER (*)(
|
|
ServiceRegistry& registry,
|
|
ApplyView& view,
|
|
AccountID const& account,
|
|
uint256 const& delIndex,
|
|
std::shared_ptr<SLE> const& sleDel,
|
|
beast::Journal j);
|
|
|
|
// Local function definitions that provides signature compatibility.
|
|
TER
|
|
offerDelete(
|
|
ServiceRegistry&,
|
|
ApplyView& view,
|
|
AccountID const& account,
|
|
uint256 const& delIndex,
|
|
std::shared_ptr<SLE> const& sleDel,
|
|
beast::Journal j)
|
|
{
|
|
return offerDelete(view, sleDel, j);
|
|
}
|
|
|
|
TER
|
|
removeSignersFromLedger(
|
|
ServiceRegistry& registry,
|
|
ApplyView& view,
|
|
AccountID const& account,
|
|
uint256 const& delIndex,
|
|
std::shared_ptr<SLE> const& sleDel,
|
|
beast::Journal j)
|
|
{
|
|
return SignerListSet::removeFromLedger(registry, view, account, j);
|
|
}
|
|
|
|
TER
|
|
removeTicketFromLedger(
|
|
ServiceRegistry&,
|
|
ApplyView& view,
|
|
AccountID const& account,
|
|
uint256 const& delIndex,
|
|
std::shared_ptr<SLE> const&,
|
|
beast::Journal j)
|
|
{
|
|
return Transactor::ticketDelete(view, account, delIndex, j);
|
|
}
|
|
|
|
TER
|
|
removeDepositPreauthFromLedger(
|
|
ServiceRegistry&,
|
|
ApplyView& view,
|
|
AccountID const&,
|
|
uint256 const& delIndex,
|
|
std::shared_ptr<SLE> const&,
|
|
beast::Journal j)
|
|
{
|
|
return DepositPreauth::removeFromLedger(view, delIndex, j);
|
|
}
|
|
|
|
TER
|
|
removeNFTokenOfferFromLedger(
|
|
ServiceRegistry&,
|
|
ApplyView& view,
|
|
AccountID const& account,
|
|
uint256 const& delIndex,
|
|
std::shared_ptr<SLE> const& sleDel,
|
|
beast::Journal)
|
|
{
|
|
if (!nft::deleteTokenOffer(view, sleDel))
|
|
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
|
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
TER
|
|
removeDIDFromLedger(
|
|
ServiceRegistry&,
|
|
ApplyView& view,
|
|
AccountID const& account,
|
|
uint256 const& delIndex,
|
|
std::shared_ptr<SLE> const& sleDel,
|
|
beast::Journal j)
|
|
{
|
|
return DIDDelete::deleteSLE(view, sleDel, account, j);
|
|
}
|
|
|
|
TER
|
|
removeOracleFromLedger(
|
|
ServiceRegistry&,
|
|
ApplyView& view,
|
|
AccountID const& account,
|
|
uint256 const&,
|
|
std::shared_ptr<SLE> const& sleDel,
|
|
beast::Journal j)
|
|
{
|
|
return OracleDelete::deleteOracle(view, sleDel, account, j);
|
|
}
|
|
|
|
TER
|
|
removeCredentialFromLedger(
|
|
ServiceRegistry&,
|
|
ApplyView& view,
|
|
AccountID const&,
|
|
uint256 const&,
|
|
std::shared_ptr<SLE> const& sleDel,
|
|
beast::Journal j)
|
|
{
|
|
return credentials::deleteSLE(view, sleDel, j);
|
|
}
|
|
|
|
TER
|
|
removeDelegateFromLedger(
|
|
ServiceRegistry&,
|
|
ApplyView& view,
|
|
AccountID const&,
|
|
uint256 const&,
|
|
std::shared_ptr<SLE> const& sleDel,
|
|
beast::Journal j)
|
|
{
|
|
return DelegateSet::deleteDelegate(view, sleDel, j);
|
|
}
|
|
|
|
// Return nullptr if the LedgerEntryType represents an obligation that can't
|
|
// be deleted. Otherwise return the pointer to the function that can delete
|
|
// the non-obligation
|
|
DeleterFuncPtr
|
|
nonObligationDeleter(LedgerEntryType t)
|
|
{
|
|
switch (t)
|
|
{
|
|
case ltOFFER:
|
|
return offerDelete;
|
|
case ltSIGNER_LIST:
|
|
return removeSignersFromLedger;
|
|
case ltTICKET:
|
|
return removeTicketFromLedger;
|
|
case ltDEPOSIT_PREAUTH:
|
|
return removeDepositPreauthFromLedger;
|
|
case ltNFTOKEN_OFFER:
|
|
return removeNFTokenOfferFromLedger;
|
|
case ltDID:
|
|
return removeDIDFromLedger;
|
|
case ltORACLE:
|
|
return removeOracleFromLedger;
|
|
case ltCREDENTIAL:
|
|
return removeCredentialFromLedger;
|
|
case ltDELEGATE:
|
|
return removeDelegateFromLedger;
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TER
|
|
AccountDelete::preclaim(PreclaimContext const& ctx)
|
|
{
|
|
AccountID const account{ctx.tx[sfAccount]};
|
|
AccountID const dst{ctx.tx[sfDestination]};
|
|
|
|
auto sleDst = ctx.view.read(keylet::account(dst));
|
|
|
|
if (!sleDst)
|
|
return tecNO_DST;
|
|
|
|
if ((((*sleDst)[sfFlags] & lsfRequireDestTag) != 0u) && !ctx.tx[~sfDestinationTag])
|
|
return tecDST_TAG_NEEDED;
|
|
|
|
// If credentials are provided - check them anyway
|
|
if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); !isTesSuccess(err))
|
|
return err;
|
|
|
|
// if credentials then postpone auth check to doApply, to check for expired
|
|
// credentials
|
|
if (!ctx.tx.isFieldPresent(sfCredentialIDs))
|
|
{
|
|
// Check whether the destination account requires deposit authorization.
|
|
if ((sleDst->getFlags() & lsfDepositAuth) != 0u)
|
|
{
|
|
if (!ctx.view.exists(keylet::depositPreauth(dst, account)))
|
|
return tecNO_PERMISSION;
|
|
}
|
|
}
|
|
|
|
auto sleAccount = ctx.view.read(keylet::account(account));
|
|
XRPL_ASSERT(sleAccount, "xrpl::AccountDelete::preclaim : non-null account");
|
|
if (!sleAccount)
|
|
return terNO_ACCOUNT;
|
|
|
|
// If an issuer has any issued NFTs resident in the ledger then it
|
|
// cannot be deleted.
|
|
if ((*sleAccount)[~sfMintedNFTokens] != (*sleAccount)[~sfBurnedNFTokens])
|
|
return tecHAS_OBLIGATIONS;
|
|
|
|
// If the account owns any NFTs it cannot be deleted.
|
|
Keylet const first = keylet::nftpageMin(account);
|
|
Keylet const last = keylet::nftpageMax(account);
|
|
|
|
auto const cp = ctx.view.read(
|
|
Keylet(ltNFTOKEN_PAGE, ctx.view.succ(first.key, last.key.next()).value_or(last.key)));
|
|
if (cp)
|
|
return tecHAS_OBLIGATIONS;
|
|
|
|
// We don't allow an account to be deleted if its sequence number
|
|
// is within 256 of the current ledger. This prevents replay of old
|
|
// transactions if this account is resurrected after it is deleted.
|
|
//
|
|
// We look at the account's Sequence rather than the transaction's
|
|
// Sequence in preparation for Tickets.
|
|
constexpr std::uint32_t kSEQ_DELTA{255};
|
|
if ((*sleAccount)[sfSequence] + kSEQ_DELTA > ctx.view.seq())
|
|
return tecTOO_SOON;
|
|
|
|
// We don't allow an account to be deleted if
|
|
// <FirstNFTokenSequence + MintedNFTokens> is within 256 of the
|
|
// current ledger. This is to prevent having duplicate NFTokenIDs after
|
|
// account re-creation.
|
|
//
|
|
// Without this restriction, duplicate NFTokenIDs can be reproduced when
|
|
// authorized minting is involved. Because when the minter mints a NFToken,
|
|
// the issuer's sequence does not change. So when the issuer re-creates
|
|
// their account and mints a NFToken, it is possible that the
|
|
// NFTokenSequence of this NFToken is the same as the one that the
|
|
// authorized minter minted in a previous ledger.
|
|
if ((*sleAccount)[~sfFirstNFTokenSequence].value_or(0) +
|
|
(*sleAccount)[~sfMintedNFTokens].value_or(0) + kSEQ_DELTA >
|
|
ctx.view.seq())
|
|
return tecTOO_SOON;
|
|
|
|
// Verify that the account does not own any objects that would prevent
|
|
// the account from being deleted.
|
|
Keylet const ownerDirKeylet{keylet::ownerDir(account)};
|
|
if (dirIsEmpty(ctx.view, ownerDirKeylet))
|
|
return tesSUCCESS;
|
|
|
|
std::shared_ptr<SLE const> sleDirNode{};
|
|
unsigned int uDirEntry{0};
|
|
uint256 dirEntry{beast::kZERO};
|
|
|
|
// Account has no directory at all. This _should_ have been caught
|
|
// by the dirIsEmpty() check earlier, but it's okay to catch it here.
|
|
if (!cdirFirst(ctx.view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry))
|
|
return tesSUCCESS;
|
|
|
|
std::uint32_t deletableDirEntryCount{0};
|
|
do
|
|
{
|
|
// Make sure any directory node types that we find are the kind
|
|
// we can delete.
|
|
auto sleItem = ctx.view.read(keylet::child(dirEntry));
|
|
if (!sleItem)
|
|
{
|
|
// Directory node has an invalid index. Bail out.
|
|
// LCOV_EXCL_START
|
|
JLOG(ctx.j.fatal()) << "AccountDelete: directory node in ledger " << ctx.view.seq()
|
|
<< " has index to object that is missing: " << to_string(dirEntry);
|
|
return tefBAD_LEDGER;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
LedgerEntryType const nodeType{safeCast<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
|
|
|
|
if (nonObligationDeleter(nodeType) == nullptr)
|
|
return tecHAS_OBLIGATIONS;
|
|
|
|
// We found a deletable directory entry. Count it. If we find too
|
|
// many deletable directory entries then bail out.
|
|
if (++deletableDirEntryCount > kMAX_DELETABLE_DIR_ENTRIES)
|
|
return tefTOO_BIG;
|
|
|
|
} while (cdirNext(ctx.view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry));
|
|
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
TER
|
|
AccountDelete::doApply()
|
|
{
|
|
auto src = view().peek(keylet::account(account_));
|
|
XRPL_ASSERT(src, "xrpl::AccountDelete::doApply : non-null source account");
|
|
|
|
auto const dstID = ctx_.tx[sfDestination];
|
|
auto dst = view().peek(keylet::account(dstID));
|
|
XRPL_ASSERT(dst, "xrpl::AccountDelete::doApply : non-null destination account");
|
|
|
|
if (!src || !dst)
|
|
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
|
|
|
if (ctx_.tx.isFieldPresent(sfCredentialIDs))
|
|
{
|
|
if (auto err =
|
|
verifyDepositPreauth(ctx_.tx, ctx_.view(), account_, dstID, dst, ctx_.journal);
|
|
!isTesSuccess(err))
|
|
return err;
|
|
}
|
|
|
|
Keylet const ownerDirKeylet{keylet::ownerDir(account_)};
|
|
auto const ter = cleanupOnAccountDelete(
|
|
view(),
|
|
ownerDirKeylet,
|
|
[&](LedgerEntryType nodeType,
|
|
uint256 const& dirEntry,
|
|
std::shared_ptr<SLE>& sleItem) -> std::pair<TER, SkipEntry> {
|
|
if (auto deleter = nonObligationDeleter(nodeType))
|
|
{
|
|
TER const result{deleter(ctx_.registry, view(), account_, dirEntry, sleItem, j_)};
|
|
|
|
return {result, SkipEntry::No};
|
|
}
|
|
|
|
// LCOV_EXCL_START
|
|
UNREACHABLE(
|
|
"xrpl::AccountDelete::doApply : undeletable item not found "
|
|
"in preclaim");
|
|
JLOG(j_.error()) << "AccountDelete undeletable item not "
|
|
"found in preclaim.";
|
|
return {tecHAS_OBLIGATIONS, SkipEntry::No};
|
|
// LCOV_EXCL_STOP
|
|
},
|
|
ctx_.journal);
|
|
if (!isTesSuccess(ter))
|
|
return ter;
|
|
|
|
// Transfer any XRP remaining after the fee is paid to the destination:
|
|
auto const remainingBalance = src->getFieldAmount(sfBalance).xrp();
|
|
(*dst)[sfBalance] = (*dst)[sfBalance] + remainingBalance;
|
|
(*src)[sfBalance] = (*src)[sfBalance] - remainingBalance;
|
|
ctx_.deliver(remainingBalance);
|
|
|
|
XRPL_ASSERT(
|
|
(*src)[sfBalance] == XRPAmount(0), "xrpl::AccountDelete::doApply : source balance is zero");
|
|
|
|
// If there's still an owner directory associated with the source account
|
|
// delete it.
|
|
if (view().exists(ownerDirKeylet) && !view().emptyDirDelete(ownerDirKeylet))
|
|
{
|
|
JLOG(j_.error()) << "AccountDelete cannot delete root dir node of " << toBase58(account_);
|
|
return tecHAS_OBLIGATIONS;
|
|
}
|
|
|
|
// Re-arm the password change fee if we can and need to.
|
|
if (remainingBalance > XRPAmount(0) && dst->isFlag(lsfPasswordSpent))
|
|
dst->clearFlag(lsfPasswordSpent);
|
|
|
|
view().update(dst);
|
|
view().erase(src);
|
|
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
void
|
|
AccountDelete::visitInvariantEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const&,
|
|
std::shared_ptr<SLE const> const&)
|
|
{
|
|
// No transaction-specific invariants yet (future work).
|
|
}
|
|
|
|
bool
|
|
AccountDelete::finalizeInvariants(
|
|
STTx const&,
|
|
TER,
|
|
XRPAmount,
|
|
ReadView const&,
|
|
beast::Journal const&)
|
|
{
|
|
// No transaction-specific invariants yet (future work).
|
|
return true;
|
|
}
|
|
|
|
} // namespace xrpl
|