Sponsored Content
Skip to content

refactor: Add transaction-specific invariant checking - #6551

Merged
bthomee merged 26 commits into
developfrom
tapanito/transaction-invariant
Apr 22, 2026
Merged

refactor: Add transaction-specific invariant checking#6551
bthomee merged 26 commits into
developfrom
tapanito/transaction-invariant

Conversation

@Tapanito

@Tapanito Tapanito commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Introduces a mechanism for individual transaction types to define their own
post-condition invariants, complementing the existing protocol-wide invariant
checks. Both transaction-specific and protocol-wide invariants always run; the
worst failure code is returned (tef > tec).

  • Add visitInvariantEntry / finalizeInvariants virtual methods on
    Transactor (two-phase visitor pattern matching protocol invariants)
  • Add checkTransactionInvariants to drive the visitor loop over modified
    ledger entries
  • Add checkInvariants on Transactor to orchestrate transaction-specific
    then protocol-wide checks
  • Add makeTransactor factory in applySteps to construct concrete
    transactors from an ApplyContext at runtime
  • Update Invariants_test::doInvariantCheck to run both invariant layers
  • Add no-op overrides to all existing transactors

Design decisions

  • visitInvariantEntry / finalizeInvariants are pure virtual: Forces
    every transactor to explicitly acknowledge transaction-specific invariants.
    This makes it difficult to add a new transaction type without considering
    what invariants it should enforce. No-op overrides are provided for all
    existing transactors as a starting point.
  • makeTransactor in applySteps.h/cpp: Natural companion to
    invoke_apply; avoids duplicating the transactions.macro plumbing in
    test code.
  • Transactor::~Transactor made public: Required by
    std::unique_ptr<Transactor> returned from makeTransactor; consistent
    with public constructors on all subclasses.

Amendment considerations

This refactoring does not require an amendment — there are no functional
changes to any invariant logic. The existing protocol invariants continue to
run exactly as before, and the new transaction-specific invariant methods are
all no-ops. Moving an invariant from the protocol layer to a transaction-specific
override also does not require an amendment, as the check itself remains
functionally identical; only its call site changes.

Note for reviewers

Important changes are in the following files:

image

This PR will be followed by multiple other PRs to refactor invariants themselves:

High Level Overview of Change

Context of Change

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

Introduce a two-phase visitor pattern (visitInvariantEntry /
finalizeInvariants) on Transactor so individual transaction types
can define their own post-condition checks.  These run before the
existing protocol-wide invariants and short-circuit on failure to
avoid misleading secondary errors.

- Add pure virtual visitInvariantEntry and finalizeInvariants to
  Transactor
- Implement checkTransactionInvariants to drive the visitor loop
- Extract checkInvariants to orchestrate transaction-specific then
  protocol-wide checks with reset-and-retry on failure
- Move failInvariantCheck from private to public in ApplyContext
@Tapanito
Tapanito force-pushed the tapanito/transaction-invariant branch from ee845b1 to 40ee1e1 Compare March 16, 2026 18:05
@Tapanito Tapanito changed the title Tapanito/transaction invariant feat: Add transaction-specific invariant checking Mar 17, 2026
@Tapanito Tapanito changed the title feat: Add transaction-specific invariant checking refactor: Add transaction-specific invariant checking Mar 17, 2026
Comment thread src/libxrpl/tx/Transactor.cpp Outdated
Rename class qualifiers in visitInvariantEntry and finalizeInvariants
definitions to match their actual transactor classes (e.g.,
DeleteAccount → AccountDelete, CancelCheck → CheckCancel).
@Tapanito

Copy link
Copy Markdown
Contributor Author

/ai-review

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple security and correctness issues in transaction invariant framework.

Review by Claude Opus 4.6 · Prompt: V12

Comment thread src/test/app/Invariants_test.cpp
Comment thread compile_commands.json Outdated
Comment thread src/libxrpl/tx/Transactor.cpp
Comment thread src/libxrpl/tx/transactors/account/SignerListSet.cpp
Comment thread src/libxrpl/tx/transactors/check/CheckCash.cpp
Comment thread include/xrpl/tx/applySteps.h Outdated
Comment thread src/test/app/Invariants_test.cpp
Comment thread src/libxrpl/tx/Transactor.cpp
Comment thread src/libxrpl/tx/Transactor.cpp
Always run both invariant checks instead of short-circuiting on
transaction invariant failure. Return the most severe failure code
(tef > tec). Also switch logger from j_ to ctx_.journal.
@Tapanito
Tapanito marked this pull request as ready for review March 23, 2026 15:46
@codecov

codecov Bot commented Mar 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.91358% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.6%. Comparing base (7c7c189) to head (dd654f6).
⚠️ Report is 2 commits behind head on develop.

Files with missing lines Patch % Lines
src/libxrpl/tx/Transactor.cpp 61.5% 10 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #6551     +/-   ##
=========================================
+ Coverage     82.5%   82.6%   +0.1%     
=========================================
  Files         1010    1010             
  Lines        79246   79566    +320     
  Branches      7529    7537      +8     
=========================================
+ Hits         65389   65696    +307     
- Misses       13857   13870     +13     
Files with missing lines Coverage Δ
include/xrpl/basics/Buffer.h 100.0% <ø> (ø)
include/xrpl/core/JobTypes.h 98.8% <ø> (ø)
include/xrpl/tx/Transactor.h 100.0% <ø> (ø)
...nclude/xrpl/tx/transactors/account/AccountDelete.h 100.0% <ø> (ø)
include/xrpl/tx/transactors/account/AccountSet.h 100.0% <ø> (ø)
...nclude/xrpl/tx/transactors/account/SetRegularKey.h 100.0% <ø> (ø)
...nclude/xrpl/tx/transactors/account/SignerListSet.h 100.0% <ø> (ø)
include/xrpl/tx/transactors/bridge/XChainBridge.h 100.0% <ø> (ø)
include/xrpl/tx/transactors/check/CheckCancel.h 100.0% <ø> (ø)
include/xrpl/tx/transactors/check/CheckCash.h 100.0% <ø> (ø)
... and 128 more

... and 2 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Tapanito

Copy link
Copy Markdown
Contributor Author

/ai-review

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new per-transaction invariant framework ships with every finalizeInvariants as a no-op stub, makeTransactor exposed in a public header with no lifetime contract, a misleading doc comment in Transactor.h, no short-circuit on invariant failure, and a missing obligation-type audit in AccountDelete for new feature types. See inline comments.

Review by Claude Opus 4.6 · Prompt: V12

Comment thread src/libxrpl/tx/transactors/account/AccountDelete.cpp
Comment thread include/xrpl/tx/Transactor.h
Comment thread include/xrpl/tx/applySteps.h Outdated
Comment thread src/libxrpl/tx/Transactor.cpp
Comment thread src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp
Comment thread src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp
Comment thread src/libxrpl/tx/transactors/account/SignerListSet.cpp
Comment thread src/libxrpl/tx/transactors/check/CheckCash.cpp
Comment thread src/libxrpl/tx/transactors/check/CheckCancel.cpp
Comment thread src/libxrpl/tx/applySteps.cpp
Comment thread include/xrpl/tx/applySteps.h Outdated

@godexsoft godexsoft left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a few questions and nits. Overall this is a good change 👍

Comment thread include/xrpl/tx/Transactor.h Outdated
Comment thread include/xrpl/tx/applySteps.h Outdated
Comment thread src/libxrpl/tx/Transactor.cpp Outdated
Comment thread src/libxrpl/tx/Transactor.cpp
Comment thread include/xrpl/tx/applySteps.h Outdated
Comment thread src/libxrpl/tx/Transactor.cpp
Comment thread src/libxrpl/tx/Transactor.cpp
Comment thread src/test/app/Invariants_test.cpp

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 137 out of 137 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/libxrpl/tx/applySteps.cpp
Comment thread src/libxrpl/tx/Transactor.cpp
@github-actions

Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

Addresses a review comment: the soft BEAST_EXPECT assertion would not
abort on failure, risking a null pointer dereference in the subsequent
checkInvariants call. Use an early-return guard instead.
Add direct includes for std::shared_ptr, SLE, STTx, and XRPAmount in
transactor translation units that override visitInvariantEntry or
finalizeInvariants. Previously these types were pulled in transitively,
which misc-include-cleaner flags as errors under the project's
clang-tidy configuration.
…on-invariant

# Conflicts:
#	src/test/app/Invariants_test.cpp

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean refactor — no concerns.

Review by Claude Opus 4.6 · Prompt: V15

Callers must use isDelete rather than after == nullptr to detect
deletions; after is non-null for erased SLEs as supplied by the apply
logic.
The fatal log emitted when finalizeInvariants returns false previously
lacked identifying information, making postmortems difficult. Include
the full transaction JSON, matching the global invariant checker.
@github-actions

Copy link
Copy Markdown

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean changes

Review by Claude Opus 4.6 · Prompt: V15

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues.

Review by Claude Opus 4.6 · Prompt: V15

Use container::empty() and container::contains() instead of size() == 0
and find() != end(), matching the readability-container-size-empty and
readability-container-contains checks.
Add direct includes for std::exception, std::distance, beast::Journal,
xrpl::ReadView, xrpl::Serializer, xrpl::SerialIter, xrpl::Number, and
xrpl::roundToAsset across files that use them. Also suppress
modernize-use-ranges on a std::is_sorted call where the ranges version
does not compile because SignerEntry is not std::totally_ordered.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues.

Review by Claude Opus 4.6 · Prompt: V15

Serializer.h, STAmount.h, Number.h, and <iterator> are only used inside
XRPL_ASSERT or #ifdef DEBUG blocks that compile to nothing in release
builds. Without the pragma, include-cleaner flags them as unused in
release CI while flagging them as missing in debug builds.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

Review by Claude Opus 4.6 · Prompt: V15

checkTransactionInvariants only returns tecINVARIANT_FAILED or the
input result, never tefINVARIANT_FAILED. Only protocol invariants can
escalate to tef, so the txResult comparison was dead code.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues.

Review by Claude Opus 4.6 · Prompt: V15

Comment on lines +1163 to +1169
// Protocol invariants second (broader). These check properties that must hold regardless of
// transaction type.
auto const protoResult = ctx_.checkInvariants(result, fee);

// Fail if either check failed. tef (fatal) takes priority over tec.
if (protoResult == tefINVARIANT_FAILED)
return tefINVARIANT_FAILED;

@bthomee bthomee Apr 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You first check the tx invariants, but if the proto invariants result in a fatal failure then the tx invariants were checked needlessly.

I'd recommend to rewrite this as follows:

auto const protoResult = ctx_.checkInvariants(result, fee);
if (protoResult == tefINVARIANT_FAILED || protoResult == tecINVARIANT_FAILED)
    return protoResult;

auto const txResult = checkTransactionInvariants(result, fee);
if (txResult == tecINVARIANT_FAILED)
  return txResult;

return result;

btw is protoResult guaranteed to only return either tefINVARIANT_FAILED, tecINVARIANT_FAILED or success, and is txResult guaranteed to only return tecINVARIANT_FAILED or success? If not, then you're ignoring other failures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I agree with the idea, I tried this approach already. It does not work.

The main problem, is that invariant tests are written with the idea that multiple invariant failures can appear at the same time. By skipping transaction invariants after global invariants (or global invariants after transaction invariants) we break a lot of invariant tests.

Since we are touching sensitive files, I am very reluctant to change unit-tests that serve as our mechanism of verification that nothing broke.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. We may consider doing this later if this optimization has meaningful impact. With extensive testing and running the change on a node for a while, we should be confident that we did it right.

@bthomee
bthomee added this pull request to the merge queue Apr 22, 2026
Merged via the queue into develop with commit 2e30732 Apr 22, 2026
3 checks passed
@bthomee
bthomee deleted the tapanito/transaction-invariant branch April 22, 2026 14:58
marek-foss-neti pushed a commit to marek-foss-neti/rippled that referenced this pull request May 5, 2026
@mvadari mvadari added this to the 3.2.0 milestone May 20, 2026
beartec-jpg pushed a commit to beartec-jpg/FalconLedger that referenced this pull request Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants