Skip to content

Add vault invariants#5518

Merged
bthomee merged 64 commits intodevelopfrom
Bronek/Vault_invariants
Oct 8, 2025
Merged

Add vault invariants#5518
bthomee merged 64 commits intodevelopfrom
Bronek/Vault_invariants

Conversation

@Bronek
Copy link
Copy Markdown
Collaborator

@Bronek Bronek commented Jun 30, 2025

High Level Overview of Change

This adds invariants for SingleAssetVault #5224 XLS-065

Context of Change

Invariants were intentionally skipped from #5224 to keep the PR size manageable. This PR fills the hole.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Performance (increase or change in throughput and/or latency)
  • Tests (you added tests for code that already exists, or your new feature included in this PR)
  • Documentation update
  • Chore (no impact to binary, e.g. .gitignore, formatting, dropping support for older tooling)
  • Release

@Bronek Bronek force-pushed the Bronek/Vault_invariants branch from 66d84da to ac93434 Compare June 30, 2025 12:27
@Bronek Bronek force-pushed the Bronek/Vault_invariants branch from 27470c6 to a5cd565 Compare September 5, 2025 10:25
@Bronek Bronek added Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required. and removed Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required. labels Sep 29, 2025
Comment on lines +2224 to +2229
// If `before` is empty, this means an object is being created
// If `after` is empty, this means an object is being deleted
// Otherwise it's a modification of an object.
XRPL_ASSERT(
before != nullptr || after != nullptr,
"ripple::ValidVault::visitEntry : some object is available");
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.

This comment is not true. after should never be null. Deletion is indicated by isDelete.

  • If before is empty, then the object is being created
  • If isDelete is true, the the object is being deleted
  • Otherwise it's a modification of an object.

I think the correct assert is more like

after != nullptr && (before != nullptr || !isDelete)

Comment on lines +2486 to +2490
if (txnType == ttVAULT_CREATE || //
txnType == ttVAULT_SET || //
txnType == ttVAULT_DEPOSIT || //
txnType == ttVAULT_WITHDRAW || //
txnType == ttVAULT_CLAWBACK)
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.

This should probably be a "permission"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, added mustModifyVault (and a TODO to add mayModifyVault which I think you will do)

Comment on lines +2502 to +2511
// Transactor makes this condition impossible, but since we need to
// access beforeVault_ then we also need a defensive check.
if (!beforeVault_ && txnType != ttVAULT_CREATE)
{
// LCOV_EXCL_START
UNREACHABLE(
"ripple::ValidVault::finalize : missing old vault state");
return false;
// LCOV_EXCL_STOP
}
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.

Remember, the whole purpose of the invariants are to catch things that might be wrong in transactors, so something like "Transactor makes this condition impossible" is the wrong way to look at this. All of these failure conditions should be impossible.

Also, this is absolutely testable using the framework in Invariants_test.cpp. Do a test case that manually creates, modifies, or deletes a vault, but have the transaction type be, I dunno, ttPayment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you, you are almost right - except I have to use a transaction type which passes the if in the outer scope i.e. one of the Vault transactions.

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.

Thank you, you are almost right - except I have to use a transaction type which passes the if in the outer scope i.e. one of the Vault transactions.

In that case, you should probably check condition this outside of that scope, which it looks like you've already done. Nice!

Comment on lines +2533 to +2535
(tx.getTxnType() == ttVAULT_DEPOSIT || //
tx.getTxnType() == ttVAULT_WITHDRAW || //
tx.getTxnType() == ttVAULT_CLAWBACK))
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.

This could probably also be a permission.

Comment on lines +2243 to +2245
case ltVAULT:
beforeVault_ = Vault::make(*before);
break;
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.

This whole class has a "fatal flaw". (Well, not really fatal, but very significant.)

You're assuming that only one Vault will be affected. What if there are more? You're going to, for example, overwrite befaultVault_ here.

You've got the same problem when you're looking up assets and shares in finalize, where you return the first matching item found. What if there are multiple matches?

It should be impossible, but that is what invariants are here to confirm.

All of these structures and functions should be using collections (vector, set, whatever) instead of single values. You could have a simple check after each one that's like

if (beforeVault_.size() > 1)
{
    XRPL_ASSERT(enforce, ...);
    return !enforce;
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, that's great feedback. Making it work is actually simple (since optional is basically a collection of at-most-one)

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.

Thanks, that's great feedback. Making it work is actually simple (since optional is basically a collection of at-most-one)

I figured it would be. 😄

@Bronek Bronek requested review from a team and ximinez September 30, 2025 13:58
@Bronek
Copy link
Copy Markdown
Collaborator Author

Bronek commented Oct 1, 2025

@ximinez I implemented your feedback in last three commits:
9999f3c Review feedback, yet to extend unit tests
3603356 Added mustModifyVault priviledge, code cleanup, more tests
0e58a15 More tests

Incidentally, your observation that the comment (and assert) in ValidVault::visitEntry was wrong helped me to simplify ValidVault::finalize . Thanks !

ximinez
ximinez previously requested changes Oct 1, 2025
Comment on lines +2401 to +2404
// Note, only valid case when `afterVault_` can be empty is handled above
if (afterVault_.empty())
{
if (hasPrivilege(tx, mustModifyVault))
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.

This comment is not true anymore. There's no case above that returns success if afterVault_ is empty, and this test does.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you, I cleaned this up in c73dc5f

@Bronek Bronek requested a review from ximinez October 2, 2025 09:40
Copy link
Copy Markdown
Collaborator

@ximinez ximinez left a comment

Choose a reason for hiding this comment

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

Updates look good. I don't think I've reviewed everything, so I'll skip a full approval.

@ximinez ximinez dismissed their stale review October 3, 2025 19:16

Requested changes have been made, but I haven't done a full review.

@Bronek Bronek added the Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required. label Oct 8, 2025
@Bronek Bronek added this to the 3.0.0 milestone Oct 8, 2025
@bthomee bthomee enabled auto-merge (squash) October 8, 2025 14:40
@bthomee bthomee merged commit 5ecde3c into develop Oct 8, 2025
38 checks passed
@bthomee bthomee deleted the Bronek/Vault_invariants branch October 8, 2025 15:04
Bronek added a commit that referenced this pull request Oct 27, 2025
This change adds invariants for SingleAssetVault #5224 (XLS-065), which had been intentionally skipped earlier to keep the SAV PR size manageable.
@Bronek Bronek mentioned this pull request Oct 27, 2025
1 task
Bronek added a commit that referenced this pull request Oct 31, 2025
This change adds invariants for SingleAssetVault #5224 (XLS-065), which had been intentionally skipped earlier to keep the SAV PR size manageable.
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.

7 participants