refactor: Start making ledger helpers type-safe, beginning with AccountRoot#6620
refactor: Start making ledger helpers type-safe, beginning with AccountRoot#6620mvadari wants to merge 91 commits into
AccountRoot#6620Conversation
|
/ai-review |
| return asset.visit( | ||
| [&](Issue const& issue) -> Rate { | ||
| if (isXRP(issue.account) || issue.account == dstAccount) | ||
| return kParityRate; | ||
| return transferRate(view, issue.account); | ||
| }, | ||
| [&](MPTIssue const& mptIssue) -> Rate { | ||
| [&](Issue const& issue) { return AccountRoot(issue.account, view).transferRate(); }, | ||
| [&](MPTIssue const& mptIssue) { |
| **`SLEBase<ReadView>`** holds a `std::shared_ptr<SLE const>` and a `ReadView const&`. Write-only members are excluded at compile time. | ||
| holds a mutable `std::shared_ptr<SLE>`, an `ApplyView&`, and a `Keylet`. It exposes `insert()`, `update()`, `erase()`, and `newSLE()` to keep the SLE and its view in sync automatically. |
| beast::WrappedSink sink_; | ||
| beast::Journal const j_; | ||
|
|
||
| AccountID const accountID_; | ||
| WAccountRoot account_; | ||
| XRPAmount preFeeBalance_{}; // Balance before fees. |
There was a problem hiding this comment.
ℹ️ Note: This is a large diff (293,703 chars). Complex issues deep in the diff may receive less attention.
One high-severity correctness issue (dropped dstAccount parity guard in BookStep.cpp), plus missing null-check, redundant view lookup, dropped journal, and a doc formatting gap — see inline comments.
Review by Claude Sonnet 4.6 · Prompt: V15
| `SLEBase.h` introduces a single template class `SLEBase<ViewT>` that pairs an SLE with its view context and enforces read/write semantics at compile time via `requires` clauses. | ||
|
|
||
| **`SLEBase<ReadView>`** holds a `std::shared_ptr<SLE const>` and a `ReadView const&`. Write-only members are excluded at compile time. | ||
| holds a mutable `std::shared_ptr<SLE>`, an `ApplyView&`, and a `Keylet`. It exposes `insert()`, `update()`, `erase()`, and `newSLE()` to keep the SLE and its view in sync automatically. |
There was a problem hiding this comment.
Missing SLEBase<ApplyView> header — line reads as continuation of the SLEBase<ReadView> description above:
| holds a mutable `std::shared_ptr<SLE>`, an `ApplyView&`, and a `Keylet`. It exposes `insert()`, `update()`, `erase()`, and `newSLE()` to keep the SLE and its view in sync automatically. | |
| **`SLEBase<ApplyView>`** holds a mutable `std::shared_ptr<SLE>`, an `ApplyView&`, and a `Keylet`. It exposes `insert()`, `update()`, `erase()`, and `newSLE()` to keep the SLE and its view in sync automatically. |
| beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) | ||
| requires kIsWritable | ||
| : view_(view) | ||
| , key_(sle ? Keylet(sle->getType(), sle->key()) : Keylet(ltANY, uint256{})) |
There was a problem hiding this comment.
Null sle produces a bogus Keylet(ltANY, uint256{}), which newSLE() would silently use. Assert non-null instead:
| , key_(sle ? Keylet(sle->getType(), sle->key()) : Keylet(ltANY, uint256{})) | |
| , key_(sle ? Keylet(sle->getType(), sle->key()) : (XRPL_ASSERT(false, "xrpl::SLEBase : null sle passed to writable constructor"), Keylet(ltANY, uint256{}))) |
| return transferRate(view, issue.account); | ||
| }, | ||
| [&](MPTIssue const& mptIssue) -> Rate { | ||
| [&](Issue const& issue) { return AccountRoot(issue.account, view).transferRate(); }, |
There was a problem hiding this comment.
issue.account == dstAccount parity guard was dropped. When the IOU issuer is the destination, the old code returned kParityRate (fee-free); AccountRoot::transferRate() never checks dstAccount, so a destination with sfTransferRate set will now incorrectly incur a transfer fee.
| adjustOwnerCount( | ||
| view, view.peek(accountKeylet), removeFromOwnerCount, registry.getJournal("View")); | ||
| WAccountRoot wrappedAcct(account, view, j); | ||
| wrappedAcct.adjustOwnerCount(removeFromOwnerCount); |
There was a problem hiding this comment.
Missing null-check on wrappedAcct before use. Every other WAccountRoot call site in this PR guards with if (!wrappedAcct) return tefINTERNAL;. XRPL_ASSERT is a no-op in release builds, so a missing account silently dereferences null.
WAccountRoot wrappedAcct(account, view, j);
if (!wrappedAcct)
return tefINTERNAL; // LCOV_EXCL_LINE
wrappedAcct.adjustOwnerCount(removeFromOwnerCount);
| // Have to check again in deposit() because | ||
| // amounts might be derived based on tokens or | ||
| // limits. | ||
| AccountRoot wrappedAcct(accountID, ctx.view); |
There was a problem hiding this comment.
ctx.j not passed — xrpLiquid trace-logging silently uses null sink. AMMCreate.cpp correctly passes ctx.j in the parallel change.
| AccountRoot wrappedAcct(accountID, ctx.view); | |
| AccountRoot wrappedAcct(accountID, ctx.view, ctx.j); |
| if (auto err = | ||
| verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal); | ||
| if (auto err = verifyDepositPreauth( | ||
| ctx_.tx, ctx_.view(), txAccount, AccountRoot(dst, ctx_.view(), j_), ctx_.journal); |
There was a problem hiding this comment.
Redundant view.read() — dstAcct is already in scope and converts implicitly to RAccountRoot. Pass it directly:
| ctx_.tx, ctx_.view(), txAccount, AccountRoot(dst, ctx_.view(), j_), ctx_.journal); | |
| ctx_.tx, ctx_.view(), txAccount, dstAcct, ctx_.journal); |
| return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE | ||
| auto const feePayer = ctx_.tx.getFeePayer(); | ||
| bool const hasDelegateAcct = (feePayer != accountID_); | ||
| std::optional<WAccountRoot> delegateAcct; |
There was a problem hiding this comment.
delegateAcct is already optional, this bool is redundant. All three use-sites where hasDelegateAcct is used, can use delegateAcct directly.
| /** Returns true if the ledger entry exists */ | ||
| [[nodiscard]] bool | ||
| exists() const | ||
| { | ||
| return sle_ != nullptr; | ||
| } | ||
|
|
||
| /** Explicit conversion to bool for convenient existence checking */ | ||
| explicit | ||
| operator bool() const | ||
| { | ||
| return exists(); | ||
| } |
There was a problem hiding this comment.
There are two ways to check for existence, which is already creating confusion in this PR.
In some places, like the Transactor.cpp both !*acct and !acct->exists() are used interchangeably. There's nothing in the methods indicating that they have the same semantics. Why not picking one and sticking with it?
|
|
||
| //------------------------------------------------------------------------------ | ||
|
|
||
| [[nodiscard]] static bool |
There was a problem hiding this comment.
Out of scope for this PR, but how are you thinking about invariants? Do you envision they'll operate on raw SLEs or do you have a mechanism in mind to wrap the SLEs in the new classes?
|
|
||
| auto const issuer = view.read(keylet::account(issue.getIssuer())); | ||
| if (!issuer) | ||
| if (!issuer.exists()) |
There was a problem hiding this comment.
Why are you using .exists() instead of !issuer?
|
|
||
| void | ||
| adjustOwnerCount( | ||
| std::shared_ptr<SLE> const& sle, |
There was a problem hiding this comment.
Please change this to SLE::ref, that way we won't have to bring in memory dependency.
| [[nodiscard]] static bool | ||
| isPseudoAccount(std::shared_ptr<SLE const> sleAcct) | ||
| { | ||
| auto const& fields = getPseudoAccountFields(); |
There was a problem hiding this comment.
This code, 1:1 replicates the code in the AccountRootHelpers, if the logic has to change for any reason, it'll potentially create discrepancies between the two implementations. Please move the logic to a helper function that both the AccountRootHelpers and the method here can use.
| @@ -116,6 +117,7 @@ class Transactor | |||
| beast::Journal const j_; | |||
|
|
|||
| AccountID const accountID_; | |||
There was a problem hiding this comment.
Why do we need this member variable? Surely it can be replaced with account_.id()?
| // accidentally calling this with the wrong type. | ||
| void | ||
| adjustOwnerCount( | ||
| std::shared_ptr<SLE> const& sle, |
There was a problem hiding this comment.
Ditto here, please useSLE::ref
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
|
All conflicts have been resolved. Assigned reviewers can now start or resume their review. |
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
ximinez
left a comment
There was a problem hiding this comment.
I'm just getting started on this one, so just a couple of preliminary comments.
The reason I'm posting it now is to ask how feasible it would be to split this into 2 or 3 stacked PRs?
- The base framework -
SLEBase, etc. - Maybe the
AccountRoot<ViewT>, etc., if you don't include that in the first one. - Changing all the call sites and usage from
SLEtoAccountRoot.
| static constexpr bool kIsWritable = WritableView<ViewT>; | ||
|
|
||
| // SLE pointer type: mutable for writable views, const for read-only | ||
| using sle_ptr_type = std::conditional_t<kIsWritable, std::shared_ptr<SLE>, SLE::const_pointer>; |
There was a problem hiding this comment.
std::shared_ptr<SLE> is equivalent to SLE::pointer. No point in mixing metaphors.
| using sle_ptr_type = std::conditional_t<kIsWritable, std::shared_ptr<SLE>, SLE::const_pointer>; | |
| using sle_ptr_type = std::conditional_t<kIsWritable, SLE::pointer, SLE::const_pointer>; |
| * Derived classes should provide domain-specific accessors that hide | ||
| * implementation details of the underlying ledger entry format. | ||
| */ | ||
| template <typename ViewT> |
There was a problem hiding this comment.
Instead of typename, how about another concept?
template <typename V>
concept UsableView = WritableView<V> || std::derived_from<V, ReadView>;
Maybe pick a better name than "Usable".
High Level Overview of Change
This PR marks the beginning of a process to make SLEs and SLE helpers more type-safe. The original proposal is here. The general idea is each ledger entry will have a read-only and a write-access version of the class. Getter functions will be on the read-only version, and modifier functions will be on the writable version.
This PR begins with implementing base read and write classes, and also implements and migrates
AccountRootto the type-safe architecture. To keep PRs small and easy to review, ~each ledger entry will have its own implementation/migration PR.There is no functionality change in this PR, only pure refactoring.
Note for reviewers: I recommend enabling "Hide whitespace" when looking at the diff for this PR in the Github UI.
Context of Change
There is a README added as a part of this PR
API Impact
N/A
Test Plan
Tests were not modified at all, and still pass.