Pseudo-account improvements

- Define pseudo-account fields with an sfield flag
- Pseudo-account invariant checks rules whenever a pseudo-account is
  created or modified.
This commit is contained in:
Ed Hennis
2025-04-04 16:58:31 -04:00
parent 1bbe5383ca
commit 1db5e9b4d3
6 changed files with 174 additions and 19 deletions

View File

@@ -149,7 +149,10 @@ public:
sMD_DeleteFinal = 0x04, // final value when it is deleted sMD_DeleteFinal = 0x04, // final value when it is deleted
sMD_Create = 0x08, // value when it's created sMD_Create = 0x08, // value when it's created
sMD_Always = 0x10, // value when node containing it is affected at all sMD_Always = 0x10, // value when node containing it is affected at all
sMD_BaseTen = 0x20, sMD_BaseTen =
0x20, // value is treated as base 10, overriding default behavior
sMD_PseudoAccount = 0x40, // if this field is set in an ACCOUNT_ROOT
// _only_, then it is a pseudo-account
sMD_Default = sMD_Default =
sMD_ChangeOrig | sMD_ChangeNew | sMD_DeleteFinal | sMD_Create sMD_ChangeOrig | sMD_ChangeNew | sMD_DeleteFinal | sMD_Create
}; };

View File

@@ -187,7 +187,8 @@ TYPED_SFIELD(sfNFTokenID, UINT256, 10)
TYPED_SFIELD(sfEmitParentTxnID, UINT256, 11) TYPED_SFIELD(sfEmitParentTxnID, UINT256, 11)
TYPED_SFIELD(sfEmitNonce, UINT256, 12) TYPED_SFIELD(sfEmitNonce, UINT256, 12)
TYPED_SFIELD(sfEmitHookHash, UINT256, 13) TYPED_SFIELD(sfEmitHookHash, UINT256, 13)
TYPED_SFIELD(sfAMMID, UINT256, 14) TYPED_SFIELD(sfAMMID, UINT256, 14,
SField::sMD_PseudoAccount |SField::sMD_Default)
// 256-bit (uncommon) // 256-bit (uncommon)
TYPED_SFIELD(sfBookDirectory, UINT256, 16) TYPED_SFIELD(sfBookDirectory, UINT256, 16)
@@ -209,8 +210,10 @@ TYPED_SFIELD(sfHookHash, UINT256, 31)
TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookNamespace, UINT256, 32)
TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfDomainID, UINT256, 34)
TYPED_SFIELD(sfVaultID, UINT256, 35) TYPED_SFIELD(sfVaultID, UINT256, 35,
TYPED_SFIELD(sfLoanBrokerID, UINT256, 36) SField::sMD_PseudoAccount | SField::sMD_Default)
TYPED_SFIELD(sfLoanBrokerID, UINT256, 36,
SField::sMD_PseudoAccount | SField::sMD_Default)
TYPED_SFIELD(sfLoanID, UINT256, 37) TYPED_SFIELD(sfLoanID, UINT256, 37)
// number (common) // number (common)

View File

@@ -1723,4 +1723,95 @@ NoModifiedUnmodifiableFields::finalize(
return true; return true;
} }
//------------------------------------------------------------------------------
void
ValidPseudoAccounts::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete)
// Deletion is ignored
return;
if (after && after->getType() == ltACCOUNT_ROOT)
{
bool const isPseudo = [&]() {
// isPseudoAccount checks that any of the pseudo-account fields are
// set.
if (isPseudoAccount(after))
return true;
// Not all pseudo-accounts have a zero sequence, but all accounts
// with a zero sequence had better be pseudo-accounts.
if (after->at(sfSequence) == 0)
return true;
return false;
}();
if (isPseudo)
{
// Pseudo accounts must have the following properties:
// 1. Exactly one of the pseudo-account fields is set.
// 2. The sequence number is not changed.
// 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth
// flags are set.
// 4. The RegularKey is not set.
{
std::vector<SField const*> const& fields =
getPseudoAccountFields();
auto const numFields = std::count_if(
fields.begin(),
fields.end(),
[&after](SField const* sf) -> bool {
return after->isFieldPresent(*sf);
});
if (numFields != 1)
{
std::stringstream error;
error << "pseudo-account has " << numFields
<< "pseudo-account fields set";
errors_.emplace_back(error.str());
}
}
if (before && before->at(sfSequence) != after->at(sfSequence))
{
errors_.emplace_back("pseudo-account sequence changed");
}
if (!after->isFlag(
lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth))
{
errors_.emplace_back(
"Invariant failed: pseudo-account flags are not set");
}
}
}
}
bool
ValidPseudoAccounts::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
bool const enforce = view.rules().enabled(featureLendingProtocol);
XRPL_ASSERT(
errors_.empty() || enforce,
"ripple::ValidPseudoAccounts::finalize : no bad "
"changes or enforce invariant");
if (!errors_.empty())
{
for (auto const& error : errors_)
{
JLOG(j.fatal()) << "Invariant failed: " << error;
}
if (enforce)
return false;
}
return true;
}
} // namespace ripple } // namespace ripple

View File

@@ -645,6 +645,34 @@ public:
beast::Journal const&); beast::Journal const&);
}; };
/**
* @brief Invariants: Pseudo-accounts have
*
* Pseudo-accounts have certain properties, and some of those properties are
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
* rules, and that only pseudo-accounts look like pseudo-accounts.
*
*/
class ValidPseudoAccounts
{
std::vector<std::string> errors_;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
// additional invariant checks can be declared above and then added to this // additional invariant checks can be declared above and then added to this
// tuple // tuple
using InvariantChecks = std::tuple< using InvariantChecks = std::tuple<
@@ -665,7 +693,8 @@ using InvariantChecks = std::tuple<
ValidClawback, ValidClawback,
ValidMPTIssuance, ValidMPTIssuance,
ValidPermissionedDomain, ValidPermissionedDomain,
NoModifiedUnmodifiableFields>; NoModifiedUnmodifiableFields,
ValidPseudoAccounts>;
/** /**
* @brief get a tuple of all invariant checks * @brief get a tuple of all invariant checks

View File

@@ -534,6 +534,17 @@ createPseudoAccount(
[[nodiscard]] bool [[nodiscard]] bool
isPseudoAccount(std::shared_ptr<SLE const> sleAcct); isPseudoAccount(std::shared_ptr<SLE const> sleAcct);
// Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if
// set
// Pseudo-account designator fields MUST be maintained by including the
// SField::sMD_PseudoAccount flag in the SField definition. (Don't forget to
// "| SField::sMD_Default"!) The fields do NOT need to be amendment-gated,
// since a non-active amendment will not set any field, by definition.
// Specific properties of a pseudo-account are NOT checked here, that's what
// InvariantCheck is for.
[[nodiscard]] std::vector<SField const*> const&
getPseudoAccountFields();
[[nodiscard]] inline bool [[nodiscard]] inline bool
isPseudoAccount(ReadView const& view, AccountID accountId) isPseudoAccount(ReadView const& view, AccountID accountId)
{ {

View File

@@ -1061,28 +1061,45 @@ pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey)
return beast::zero; return beast::zero;
} }
// Note, the list of the pseudo-account designator fields below MUST be // Pseudo-account designator fields MUST be maintained by including the
// maintained but it does NOT need to be amendment-gated, since a // SField::sMD_PseudoAccount flag in the SField definition. (Don't forget to
// non-active amendment will not set any field, by definition. Specific // "| SField::sMD_Default"!) The fields do NOT need to be amendment-gated,
// properties of a pseudo-account are NOT checked here, that's what // since a non-active amendment will not set any field, by definition.
// Specific properties of a pseudo-account are NOT checked here, that's what
// InvariantCheck is for. // InvariantCheck is for.
static std::array<SField const*, 2> const pseudoAccountOwnerFields = { [[nodiscard]] std::vector<SField const*> const&
&sfAMMID, // getPseudoAccountFields()
&sfVaultID, // {
}; static std::vector<SField const*> const pseudoFields = []() {
auto const ar = LedgerFormats::getInstance().findByType(ltACCOUNT_ROOT);
if (!ar)
LogicError(
"ripple::isPseudoAccount : unable to find account root ledger "
"format");
auto const& soTemplate = ar->getSOTemplate();
std::vector<SField const*> pseudoFields;
for (auto const& field : soTemplate)
{
if (field.sField().shouldMeta(SField::sMD_PseudoAccount))
pseudoFields.emplace_back(&field.sField());
}
return pseudoFields;
}();
return pseudoFields;
}
[[nodiscard]] bool [[nodiscard]] bool
isPseudoAccount(std::shared_ptr<SLE const> sleAcct) isPseudoAccount(std::shared_ptr<SLE const> sleAcct)
{ {
// auto const acctFields = std::vector<SField const*> const& fields = getPseudoAccountFields();
// LedgerFormats::getInstance().findByType(ltACCOUNT_ROOT);
// Intentionally use defensive coding here because it's cheap and makes the // Intentionally use defensive coding here because it's cheap and makes the
// semantics of true return value clean. // semantics of true return value clean.
return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT && return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT &&
std::count_if( std::count_if(
pseudoAccountOwnerFields.begin(), fields.begin(),
pseudoAccountOwnerFields.end(), fields.end(),
[&sleAcct](SField const* sf) -> bool { [&sleAcct](SField const* sf) -> bool {
return sleAcct->isFieldPresent(*sf); return sleAcct->isFieldPresent(*sf);
}) > 0; }) > 0;
@@ -1095,10 +1112,11 @@ createPseudoAccount(
uint256 const& pseudoOwnerKey, uint256 const& pseudoOwnerKey,
SField const& ownerField) SField const& ownerField)
{ {
std::vector<SField const*> const& fields = getPseudoAccountFields();
XRPL_ASSERT( XRPL_ASSERT(
std::count_if( std::count_if(
pseudoAccountOwnerFields.begin(), fields.begin(),
pseudoAccountOwnerFields.end(), fields.end(),
[&ownerField](SField const* sf) -> bool { [&ownerField](SField const* sf) -> bool {
return *sf == ownerField; return *sf == ownerField;
}) == 1, }) == 1,