Files
rippled/src/libxrpl/ledger/ApplyStateTable.cpp
2026-02-20 18:33:04 -05:00

669 lines
20 KiB
C++

#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/detail/ApplyStateTable.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/st.h>
namespace xrpl {
namespace detail {
void
ApplyStateTable::apply(RawView& to) const
{
to.rawDestroyXRP(dropsDestroyed_);
for (auto const& item : items_)
{
auto const& sle = item.second.second;
switch (item.second.first)
{
case Action::cache:
break;
case Action::erase:
to.rawErase(sle);
break;
case Action::insert:
to.rawInsert(sle);
break;
case Action::modify:
to.rawReplace(sle);
break;
};
}
}
std::size_t
ApplyStateTable::size() const
{
std::size_t ret = 0;
for (auto& item : items_)
{
switch (item.second.first)
{
case Action::erase:
case Action::insert:
case Action::modify:
++ret;
default:
break;
}
}
return ret;
}
void
ApplyStateTable::visit(
ReadView const& to,
std::function<void(
uint256 const& key,
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)> const& func) const
{
for (auto& item : items_)
{
switch (item.second.first)
{
case Action::erase:
func(item.first, true, to.read(keylet::unchecked(item.first)), item.second.second);
break;
case Action::insert:
func(item.first, false, nullptr, item.second.second);
break;
case Action::modify:
func(item.first, false, to.read(keylet::unchecked(item.first)), item.second.second);
break;
default:
break;
}
}
}
std::optional<TxMeta>
ApplyStateTable::apply(
OpenView& to,
STTx const& tx,
TER ter,
std::optional<STAmount> const& deliver,
std::optional<uint256 const> const& parentBatchId,
bool isDryRun,
beast::Journal j)
{
// Build metadata and insert
auto const sTx = std::make_shared<Serializer>();
tx.add(*sTx);
std::shared_ptr<Serializer> sMeta;
std::optional<TxMeta> metadata;
if (!to.open() || isDryRun)
{
TxMeta meta(tx.getTransactionID(), to.seq());
meta.setDeliveredAmount(deliver);
meta.setParentBatchID(parentBatchId);
Mods newMod;
for (auto& item : items_)
{
SField const* type;
switch (item.second.first)
{
default:
case Action::cache:
continue;
case Action::erase:
type = &sfDeletedNode;
break;
case Action::insert:
type = &sfCreatedNode;
break;
case Action::modify:
type = &sfModifiedNode;
break;
}
auto const origNode = to.read(keylet::unchecked(item.first));
auto curNode = item.second.second;
if ((type == &sfModifiedNode) && (*curNode == *origNode))
continue;
std::uint16_t nodeType = curNode ? curNode->getFieldU16(sfLedgerEntryType)
: origNode->getFieldU16(sfLedgerEntryType);
meta.setAffectedNode(item.first, *type, nodeType);
if (type == &sfDeletedNode)
{
XRPL_ASSERT(
origNode && curNode,
"xrpl::detail::ApplyStateTable::apply : valid nodes for "
"deletion");
threadOwners(to, meta, origNode, newMod, j);
STObject prevs(sfPreviousFields);
for (auto const& obj : *origNode)
{
// go through the original node for
// modified fields saved on modification
if (obj.getFName().shouldMeta(SField::sMD_ChangeOrig) &&
!curNode->hasMatchingEntry(obj))
prevs.emplace_back(obj);
}
if (!prevs.empty())
meta.getAffectedNode(item.first).emplace_back(std::move(prevs));
STObject finals(sfFinalFields);
for (auto const& obj : *curNode)
{
// go through the final node for final fields
if (obj.getFName().shouldMeta(SField::sMD_Always | SField::sMD_DeleteFinal))
finals.emplace_back(obj);
}
if (!finals.empty())
meta.getAffectedNode(item.first).emplace_back(std::move(finals));
}
else if (type == &sfModifiedNode)
{
XRPL_ASSERT(
curNode && origNode,
"xrpl::detail::ApplyStateTable::apply : valid nodes for "
"modification");
if (curNode->isThreadedType(to.rules())) // thread transaction to node
// item modified
threadItem(meta, curNode);
STObject prevs(sfPreviousFields);
for (auto const& obj : *origNode)
{
// search the original node for values saved on modify
if (obj.getFName().shouldMeta(SField::sMD_ChangeOrig) &&
!curNode->hasMatchingEntry(obj))
prevs.emplace_back(obj);
}
if (!prevs.empty())
meta.getAffectedNode(item.first).emplace_back(std::move(prevs));
STObject finals(sfFinalFields);
for (auto const& obj : *curNode)
{
// search the final node for values saved always
if (obj.getFName().shouldMeta(SField::sMD_Always | SField::sMD_ChangeNew))
finals.emplace_back(obj);
}
if (!finals.empty())
meta.getAffectedNode(item.first).emplace_back(std::move(finals));
}
else if (type == &sfCreatedNode) // if created, thread to owner(s)
{
XRPL_ASSERT(
curNode && !origNode,
"xrpl::detail::ApplyStateTable::apply : valid nodes for "
"creation");
threadOwners(to, meta, curNode, newMod, j);
if (curNode->isThreadedType(to.rules())) // always thread to self
threadItem(meta, curNode);
STObject news(sfNewFields);
for (auto const& obj : *curNode)
{
// save non-default values
if (!obj.isDefault() &&
obj.getFName().shouldMeta(SField::sMD_Create | SField::sMD_Always))
news.emplace_back(obj);
}
if (!news.empty())
meta.getAffectedNode(item.first).emplace_back(std::move(news));
}
else
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::detail::ApplyStateTable::apply : unsupported "
"operation type");
// LCOV_EXCL_STOP
}
}
if (!isDryRun)
{
// add any new modified nodes to the modification set
for (auto const& mod : newMod)
to.rawReplace(mod.second);
}
sMeta = std::make_shared<Serializer>();
meta.addRaw(*sMeta, ter, to.txCount());
// VFALCO For diagnostics do we want to show
// metadata even when the base view is open?
JLOG(j.trace()) << "metadata " << meta.getJson(JsonOptions::none);
metadata = meta;
}
if (!isDryRun)
{
to.rawTxInsert(tx.getTransactionID(), sTx, sMeta);
apply(to);
}
return metadata;
}
//---
bool
ApplyStateTable::exists(ReadView const& base, Keylet const& k) const
{
auto const iter = items_.find(k.key);
if (iter == items_.end())
return base.exists(k);
auto const& item = iter->second;
auto const& sle = item.second;
switch (item.first)
{
case Action::erase:
return false;
case Action::cache:
case Action::insert:
case Action::modify:
break;
}
if (!k.check(*sle))
return false;
return true;
}
auto
ApplyStateTable::succ(
ReadView const& base,
key_type const& key,
std::optional<key_type> const& last) const -> std::optional<key_type>
{
std::optional<key_type> next = key;
items_t::const_iterator iter;
// Find base successor that is
// not also deleted in our list
do
{
next = base.succ(*next, last);
if (!next)
break;
iter = items_.find(*next);
} while (iter != items_.end() && iter->second.first == Action::erase);
// Find non-deleted successor in our list
for (iter = items_.upper_bound(key); iter != items_.end(); ++iter)
{
if (iter->second.first != Action::erase)
{
// Found both, return the lower key
if (!next || next > iter->first)
next = iter->first;
break;
}
}
// Nothing in our list, return
// what we got from the parent.
if (last && next >= last)
return std::nullopt;
return next;
}
std::shared_ptr<SLE const>
ApplyStateTable::read(ReadView const& base, Keylet const& k) const
{
auto const iter = items_.find(k.key);
if (iter == items_.end())
return base.read(k);
auto const& item = iter->second;
auto const& sle = item.second;
switch (item.first)
{
case Action::erase:
return nullptr;
case Action::cache:
case Action::insert:
case Action::modify:
break;
};
if (!k.check(*sle))
return nullptr;
return sle;
}
std::optional<std::shared_ptr<SLE const>>
ApplyStateTable::readLocal(Keylet const& k) const
{
auto const iter = items_.find(k.key);
if (iter == items_.end())
return std::nullopt;
auto const& item = iter->second;
auto const& sle = item.second;
switch (item.first)
{
case Action::erase:
return nullptr;
case Action::cache:
case Action::insert:
case Action::modify:
break;
};
if (!k.check(*sle))
return nullptr;
return sle;
}
std::shared_ptr<SLE>
ApplyStateTable::peek(ReadView const& base, Keylet const& k)
{
auto iter = items_.lower_bound(k.key);
if (iter == items_.end() || iter->first != k.key)
{
auto const sle = base.read(k);
if (!sle)
return nullptr;
// Make our own copy
using namespace std;
iter = items_.emplace_hint(
iter,
piecewise_construct,
forward_as_tuple(sle->key()),
forward_as_tuple(Action::cache, make_shared<SLE>(*sle)));
return iter->second.second;
}
auto const& item = iter->second;
auto const& sle = item.second;
switch (item.first)
{
case Action::erase:
return nullptr;
case Action::cache:
case Action::insert:
case Action::modify:
break;
};
if (!k.check(*sle))
return nullptr;
return sle;
}
void
ApplyStateTable::erase(ReadView const& base, std::shared_ptr<SLE> const& sle)
{
auto const iter = items_.find(sle->key());
if (iter == items_.end())
LogicError("ApplyStateTable::erase: missing key");
auto& item = iter->second;
if (item.second != sle)
LogicError("ApplyStateTable::erase: unknown SLE");
switch (item.first)
{
case Action::erase:
LogicError("ApplyStateTable::erase: double erase");
break;
case Action::insert:
items_.erase(iter);
break;
case Action::cache:
case Action::modify:
item.first = Action::erase;
break;
}
}
void
ApplyStateTable::rawErase(ReadView const& base, std::shared_ptr<SLE> const& sle)
{
using namespace std;
auto const result = items_.emplace(
piecewise_construct, forward_as_tuple(sle->key()), forward_as_tuple(Action::erase, sle));
if (result.second)
return;
auto& item = result.first->second;
switch (item.first)
{
case Action::erase:
LogicError("ApplyStateTable::rawErase: double erase");
break;
case Action::insert:
items_.erase(result.first);
break;
case Action::cache:
case Action::modify:
item.first = Action::erase;
item.second = sle;
break;
}
}
void
ApplyStateTable::insert(ReadView const& base, std::shared_ptr<SLE> const& sle)
{
auto const iter = items_.lower_bound(sle->key());
if (iter == items_.end() || iter->first != sle->key())
{
using namespace std;
items_.emplace_hint(
iter,
piecewise_construct,
forward_as_tuple(sle->key()),
forward_as_tuple(Action::insert, sle));
return;
}
auto& item = iter->second;
switch (item.first)
{
case Action::cache:
LogicError("ApplyStateTable::insert: already cached");
case Action::insert:
LogicError("ApplyStateTable::insert: already inserted");
case Action::modify:
LogicError("ApplyStateTable::insert: already modified");
case Action::erase:
break;
}
item.first = Action::modify;
item.second = sle;
}
void
ApplyStateTable::replace(ReadView const& base, std::shared_ptr<SLE> const& sle)
{
auto const iter = items_.lower_bound(sle->key());
if (iter == items_.end() || iter->first != sle->key())
{
using namespace std;
items_.emplace_hint(
iter,
piecewise_construct,
forward_as_tuple(sle->key()),
forward_as_tuple(Action::modify, sle));
return;
}
auto& item = iter->second;
switch (item.first)
{
case Action::erase:
LogicError("ApplyStateTable::replace: already erased");
case Action::cache:
item.first = Action::modify;
break;
case Action::insert:
case Action::modify:
break;
}
item.second = sle;
}
void
ApplyStateTable::update(ReadView const& base, std::shared_ptr<SLE> const& sle)
{
auto const iter = items_.find(sle->key());
if (iter == items_.end())
LogicError("ApplyStateTable::update: missing key");
auto& item = iter->second;
if (item.second != sle)
LogicError("ApplyStateTable::update: unknown SLE");
switch (item.first)
{
case Action::erase:
LogicError("ApplyStateTable::update: erased");
break;
case Action::cache:
item.first = Action::modify;
break;
case Action::insert:
case Action::modify:
break;
};
}
void
ApplyStateTable::destroyXRP(XRPAmount const& fee)
{
dropsDestroyed_ += fee;
}
//------------------------------------------------------------------------------
// Insert this transaction to the SLE's threading list
void
ApplyStateTable::threadItem(TxMeta& meta, std::shared_ptr<SLE> const& sle)
{
key_type prevTxID;
LedgerIndex prevLgrID;
if (!sle->thread(meta.getTxID(), meta.getLgrSeq(), prevTxID, prevLgrID))
return;
if (!prevTxID.isZero())
{
auto& node = meta.getAffectedNode(sle, sfModifiedNode);
if (node.getFieldIndex(sfPreviousTxnID) == -1)
{
XRPL_ASSERT(
node.getFieldIndex(sfPreviousTxnLgrSeq) == -1,
"xrpl::ApplyStateTable::threadItem : previous ledger is not "
"set");
node.setFieldH256(sfPreviousTxnID, prevTxID);
node.setFieldU32(sfPreviousTxnLgrSeq, prevLgrID);
}
XRPL_ASSERT(
node.getFieldH256(sfPreviousTxnID) == prevTxID,
"xrpl::ApplyStateTable::threadItem : previous transaction is a "
"match");
XRPL_ASSERT(
node.getFieldU32(sfPreviousTxnLgrSeq) == prevLgrID,
"xrpl::ApplyStateTable::threadItem : previous ledger is a match");
}
}
std::shared_ptr<SLE>
ApplyStateTable::getForMod(ReadView const& base, key_type const& key, Mods& mods, beast::Journal j)
{
{
auto miter = mods.find(key);
if (miter != mods.end())
{
XRPL_ASSERT(miter->second, "xrpl::ApplyStateTable::getForMod : non-null result");
return miter->second;
}
}
{
auto iter = items_.find(key);
if (iter != items_.end())
{
auto const& item = iter->second;
if (item.first == Action::erase)
{
// The Destination of an Escrow or a PayChannel may have been
// deleted. In that case the account we're threading to will
// not be found and it is appropriate to return a nullptr.
JLOG(j.warn()) << "Trying to thread to deleted node";
return nullptr;
}
if (item.first != Action::cache)
return item.second;
// If it's only cached, then the node is being modified only by
// metadata; fall through and track it in the mods table.
}
}
auto c = base.read(keylet::unchecked(key));
if (!c)
{
// The Destination of an Escrow or a PayChannel may have been
// deleted. In that case the account we're threading to will
// not be found and it is appropriate to return a nullptr.
JLOG(j.warn()) << "ApplyStateTable::getForMod: key not found";
return nullptr;
}
auto sle = std::make_shared<SLE>(*c);
mods.emplace(key, sle);
return sle;
}
void
ApplyStateTable::threadTx(
ReadView const& base,
TxMeta& meta,
AccountID const& to,
Mods& mods,
beast::Journal j)
{
auto const sle = getForMod(base, keylet::account(to).key, mods, j);
if (!sle)
{
// The Destination of an Escrow or PayChannel may have been deleted.
// In that case the account we are threading to will not be found.
// So this logging is just a warning.
JLOG(j.warn()) << "Threading to non-existent account: " << toBase58(to);
return;
}
// threadItem only applied to AccountRoot
XRPL_ASSERT(
sle->isThreadedType(base.rules()), "xrpl::ApplyStateTable::threadTx : SLE is threaded");
threadItem(meta, sle);
}
void
ApplyStateTable::threadOwners(
ReadView const& base,
TxMeta& meta,
std::shared_ptr<SLE const> const& sle,
Mods& mods,
beast::Journal j)
{
LedgerEntryType const ledgerType{sle->getType()};
switch (ledgerType)
{
case ltACCOUNT_ROOT: {
// Nothing to do
break;
}
case ltRIPPLE_STATE: {
threadTx(base, meta, (*sle)[sfLowLimit].getIssuer(), mods, j);
threadTx(base, meta, (*sle)[sfHighLimit].getIssuer(), mods, j);
break;
}
default: {
// If sfAccount is present, thread to that account
if (auto const optSleAcct{(*sle)[~sfAccount]})
threadTx(base, meta, *optSleAcct, mods, j);
// If sfDestination is present, thread to that account
if (auto const optSleDest{(*sle)[~sfDestination]})
threadTx(base, meta, *optSleDest, mods, j);
}
}
}
} // namespace detail
} // namespace xrpl