Files
rippled/src/libxrpl/tx/transactors/account/SignerListSet.cpp
2026-04-30 15:24:14 -04:00

428 lines
14 KiB
C++

#include <xrpl/tx/transactors/account/SignerListSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.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/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.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/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/SignerEntries.h>
#include <xrpl/tx/Transactor.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <tuple>
#include <utility>
#include <vector>
namespace xrpl {
// We're prepared for there to be multiple signer lists in the future,
// but we don't need them yet. So for the time being we're manually
// setting the sfSignerListID to zero in all cases.
static std::uint32_t const DEFAULT_SIGNER_LIST_ID = 0;
std::tuple<NotTEC, std::uint32_t, std::vector<SignerEntries::SignerEntry>, SignerListSet::Operation>
SignerListSet::determineOperation(STTx const& tx, ApplyFlags flags, beast::Journal j)
{
// Check the quorum. A non-zero quorum means we're creating or replacing
// the list. A zero quorum means we're destroying the list.
auto const quorum = tx[sfSignerQuorum];
std::vector<SignerEntries::SignerEntry> sign;
Operation op = Operation::unknown;
bool const hasSignerEntries(tx.isFieldPresent(sfSignerEntries));
if ((quorum != 0u) && hasSignerEntries)
{
auto signers = SignerEntries::deserialize(tx, j, "transaction");
if (!signers)
return std::make_tuple(signers.error(), quorum, sign, op);
std::sort(signers->begin(), signers->end());
// Save deserialized list for later.
sign = std::move(*signers);
op = Operation::set;
}
else if ((quorum == 0) && !hasSignerEntries)
{
op = Operation::destroy;
}
return std::make_tuple(tesSUCCESS, quorum, sign, op);
}
std::uint32_t
SignerListSet::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
SignerListSet::preflight(PreflightContext const& ctx)
{
auto const result = determineOperation(ctx.tx, ctx.flags, ctx.j);
if (!isTesSuccess(std::get<0>(result)))
return std::get<0>(result);
if (std::get<3>(result) == Operation::unknown)
{
// Neither a set nor a destroy. Malformed.
JLOG(ctx.j.trace()) << "Malformed transaction: Invalid signer set list format.";
return temMALFORMED;
}
if (std::get<3>(result) == Operation::set)
{
// Validate our settings.
auto const account = ctx.tx.getAccountID(sfAccount);
NotTEC const ter = validateQuorumAndSignerEntries(
std::get<1>(result), std::get<2>(result), account, ctx.j, ctx.rules);
if (!isTesSuccess(ter))
{
return ter;
}
}
return tesSUCCESS;
}
TER
SignerListSet::doApply()
{
// Perform the operation preCompute() decided on.
switch (do_)
{
case Operation::set:
return replaceSignerList();
case Operation::destroy:
return destroySignerList();
default:
break;
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::SignerListSet::doApply : invalid operation");
return temMALFORMED;
// LCOV_EXCL_STOP
}
void
SignerListSet::preCompute()
{
// Get the quorum and operation info.
auto result = determineOperation(ctx_.tx, view().flags(), j_);
XRPL_ASSERT(
isTesSuccess(std::get<0>(result)),
"xrpl::SignerListSet::preCompute : result is tesSUCCESS");
XRPL_ASSERT(
std::get<3>(result) != Operation::unknown,
"xrpl::SignerListSet::preCompute : result is known operation");
quorum_ = std::get<1>(result);
signers_ = std::get<2>(result);
do_ = std::get<3>(result);
Transactor::preCompute();
}
// The return type is signed so it is compatible with the 3rd argument
// of adjustOwnerCount() (which must be signed).
static int
signerCountBasedOwnerCountDelta(std::size_t entryCount, Rules const& rules)
{
// We always compute the full change in OwnerCount, taking into account:
// o The fact that we're adding/removing a SignerList and
// o Accounting for the number of entries in the list.
// We can get away with that because lists are not adjusted incrementally;
// we add or remove an entire list.
//
// The rule is:
// o Simply having a SignerList costs 2 OwnerCount units.
// o And each signer in the list costs 1 more OwnerCount unit.
// So, at a minimum, adding a SignerList with 1 entry costs 3 OwnerCount
// units. A SignerList with 8 entries would cost 10 OwnerCount units.
//
// The static_cast should always be safe since entryCount should always
// be in the range from 1 to 32.
// We've got a lot of room to grow.
XRPL_ASSERT(
entryCount >= STTx::minMultiSigners,
"xrpl::signerCountBasedOwnerCountDelta : minimum signers");
XRPL_ASSERT(
entryCount <= STTx::maxMultiSigners,
"xrpl::signerCountBasedOwnerCountDelta : maximum signers");
return 2 + static_cast<int>(entryCount);
}
static TER
removeSignersFromLedger(
ServiceRegistry& registry,
ApplyView& view,
Keylet const& accountKeylet,
Keylet const& ownerDirKeylet,
Keylet const& signerListKeylet,
beast::Journal j)
{
// We have to examine the current SignerList so we know how much to
// reduce the OwnerCount.
SLE::pointer const signers = view.peek(signerListKeylet);
// If the signer list doesn't exist we've already succeeded in deleting it.
if (!signers)
return tesSUCCESS;
// There are two different ways that the OwnerCount could be managed.
// If the lsfOneOwnerCount bit is set then remove just one owner count.
// Otherwise use the pre-MultiSignReserve amendment calculation.
int removeFromOwnerCount = -1;
if ((signers->getFlags() & lsfOneOwnerCount) == 0)
{
STArray const& actualList = signers->getFieldArray(sfSignerEntries);
removeFromOwnerCount =
signerCountBasedOwnerCountDelta(actualList.size(), view.rules()) * -1;
}
// Remove the node from the account directory.
auto const hint = (*signers)[sfOwnerNode];
if (!view.dirRemove(ownerDirKeylet, hint, signerListKeylet.key, false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete SignerList from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
adjustOwnerCount(
view, view.peek(accountKeylet), removeFromOwnerCount, registry.getJournal("View"));
view.erase(signers);
return tesSUCCESS;
}
TER
SignerListSet::removeFromLedger(
ServiceRegistry& registry,
ApplyView& view,
AccountID const& account,
beast::Journal j)
{
auto const accountKeylet = keylet::account(account);
auto const ownerDirKeylet = keylet::ownerDir(account);
auto const signerListKeylet = keylet::signerList(account);
return removeSignersFromLedger(
registry, view, accountKeylet, ownerDirKeylet, signerListKeylet, j);
}
NotTEC
SignerListSet::validateQuorumAndSignerEntries(
std::uint32_t quorum,
std::vector<SignerEntries::SignerEntry> const& signers,
AccountID const& account,
beast::Journal j,
Rules const& rules)
{
// Reject if there are too many or too few entries in the list.
{
std::size_t const signerCount = signers.size();
if (signerCount < STTx::minMultiSigners || signerCount > STTx::maxMultiSigners)
{
JLOG(j.trace()) << "Too many or too few signers in signer list.";
return temMALFORMED;
}
}
// Make sure there are no duplicate signers.
XRPL_ASSERT(
std::ranges::is_sorted(signers),
"xrpl::SignerListSet::validateQuorumAndSignerEntries : sorted "
"signers");
if (std::ranges::adjacent_find(signers) != signers.end())
{
JLOG(j.trace()) << "Duplicate signers in signer list";
return temBAD_SIGNER;
}
// Make sure no signers reference this account. Also make sure the
// quorum can be reached.
std::uint64_t allSignersWeight(0);
for (auto const& signer : signers)
{
std::uint32_t const weight = signer.weight;
if (weight <= 0)
{
JLOG(j.trace()) << "Every signer must have a positive weight.";
return temBAD_WEIGHT;
}
allSignersWeight += signer.weight;
if (signer.account == account)
{
JLOG(j.trace()) << "A signer may not self reference account.";
return temBAD_SIGNER;
}
// Don't verify that the signer accounts exist. Non-existent accounts
// may be phantom accounts (which are permitted).
}
if ((quorum <= 0) || (allSignersWeight < quorum))
{
JLOG(j.trace()) << "Quorum is unreachable";
return temBAD_QUORUM;
}
return tesSUCCESS;
}
TER
SignerListSet::replaceSignerList()
{
auto const accountKeylet = keylet::account(account_);
auto const ownerDirKeylet = keylet::ownerDir(account_);
auto const signerListKeylet = keylet::signerList(account_);
// This may be either a create or a replace. Preemptively remove any
// old signer list. May reduce the reserve, so this is done before
// checking the reserve.
if (TER const ter = removeSignersFromLedger(
ctx_.registry, view(), accountKeylet, ownerDirKeylet, signerListKeylet, j_))
return ter;
auto const sle = view().peek(accountKeylet);
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
// Compute new reserve. Verify the account has funds to meet the reserve.
std::uint32_t const oldOwnerCount{(*sle)[sfOwnerCount]};
constexpr int addedOwnerCount = 1;
std::uint32_t const flags{lsfOneOwnerCount};
XRPAmount const newReserve{view().fees().accountReserve(oldOwnerCount + addedOwnerCount)};
// We check the reserve against the starting balance because we want to
// allow dipping into the reserve to pay fees. This behavior is consistent
// with TicketCreate.
if (preFeeBalance_ < newReserve)
return tecINSUFFICIENT_RESERVE;
// Everything's ducky. Add the ltSIGNER_LIST to the ledger.
auto signerList = std::make_shared<SLE>(signerListKeylet);
view().insert(signerList);
writeSignersToSLE(signerList, flags);
auto viewJ = ctx_.registry.get().getJournal("View");
// Add the signer list to the account's directory.
auto const page =
ctx_.view().dirInsert(ownerDirKeylet, signerListKeylet, describeOwnerDir(account_));
JLOG(j_.trace()) << "Create signer list for account " << toBase58(account_) << ": "
<< (page ? "success" : "failure");
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
signerList->setFieldU64(sfOwnerNode, *page);
// If we succeeded, the new entry counts against the
// creator's reserve.
adjustOwnerCount(view(), sle, addedOwnerCount, viewJ);
return tesSUCCESS;
}
TER
SignerListSet::destroySignerList()
{
auto const accountKeylet = keylet::account(account_);
// Destroying the signer list is only allowed if either the master key
// is enabled or there is a regular key.
SLE::pointer const ledgerEntry = view().peek(accountKeylet);
if (!ledgerEntry)
return tefINTERNAL; // LCOV_EXCL_LINE
if ((ledgerEntry->isFlag(lsfDisableMaster)) && (!ledgerEntry->isFieldPresent(sfRegularKey)))
return tecNO_ALTERNATIVE_KEY;
auto const ownerDirKeylet = keylet::ownerDir(account_);
auto const signerListKeylet = keylet::signerList(account_);
return removeSignersFromLedger(
ctx_.registry, view(), accountKeylet, ownerDirKeylet, signerListKeylet, j_);
}
void
SignerListSet::writeSignersToSLE(SLE::pointer const& ledgerEntry, std::uint32_t flags) const
{
// Assign the quorum, default SignerListID, and flags.
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
ledgerEntry->setAccountID(sfOwner, account_);
}
ledgerEntry->setFieldU32(sfSignerQuorum, quorum_);
ledgerEntry->setFieldU32(sfSignerListID, DEFAULT_SIGNER_LIST_ID);
if (flags != 0u) // Only set flags if they are non-default (default is zero).
ledgerEntry->setFieldU32(sfFlags, flags);
// Create the SignerListArray one SignerEntry at a time.
STArray toLedger(signers_.size());
for (auto const& entry : signers_)
{
toLedger.push_back(STObject::makeInnerObject(sfSignerEntry));
STObject& obj = toLedger.back();
obj.reserve(2);
obj[sfAccount] = entry.account;
obj[sfSignerWeight] = entry.weight;
// This is a defensive check to make absolutely sure we will never write
// a tag into the ledger.
if (entry.tag)
obj.setFieldH256(sfWalletLocator, *(entry.tag));
}
// Assign the SignerEntries.
ledgerEntry->setFieldArray(sfSignerEntries, toLedger);
}
void
SignerListSet::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
}
bool
SignerListSet::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl