Skip to content

feat: Add invariant checks to ensure only fields that are expected to be modified are modified#7029

Open
mvadari wants to merge 29 commits into
developfrom
mvadari/constant-field-invariant
Open

feat: Add invariant checks to ensure only fields that are expected to be modified are modified#7029
mvadari wants to merge 29 commits into
developfrom
mvadari/constant-field-invariant

Conversation

@mvadari

@mvadari mvadari commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

High Level Overview of Change

This PR updates NoModifiedUnmodifiableFields to be more general and handle all ledger entries. To make this better-documented, it also updates ledger_entries.macro (and SOTemplate) to take an optional SOEConstant parameter, which determines if the field is allowed to be updated or not.

This change is amendment-gated, with a fix amendment (this can be changed to a feature amendment if needed).

Context of Change

Improving invariants

API Impact

N/A

@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.

One annotation omitted — see inline comment.


Review by ReviewBot 🤖

Review by Claude Opus 4.6 · Prompt: V15

Comment thread src/libxrpl/protocol/InnerObjectFormats.cpp Outdated

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

This PR generalizes the NoModifiedUnmodifiableFields invariant to use ledger-entry SOTemplate metadata (via a new SOEConstant annotation) so invariant enforcement can be driven by ledger format definitions instead of hardcoded per-ledger-type checks.

Changes:

  • Add SOEConstant metadata to SOElement/SOTemplate and annotate ledger-entry and inner-object format fields as constant vs mutable.
  • Update NoModifiedUnmodifiableFields to perform a template-driven “constant fields unchanged” check (gated by featureInvariantsV1_1 for enforcement).
  • Extend/adjust invariant unit tests to cover template-based behavior and updated failure expectations.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/test/app/Invariants_test.cpp Adds new coverage for template-based constant/not-constant behavior and updates expected invariant outcomes/messages.
src/libxrpl/tx/invariants/InvariantCheck.cpp Implements the template-based constant-field invariant check alongside the legacy hardcoded path.
src/libxrpl/protocol/LedgerFormats.cpp Annotates common ledger fields (sfLedgerIndex, sfLedgerEntryType, sfFlags) with constant/mutable metadata.
src/libxrpl/protocol/InnerObjectFormats.cpp Adds constant/mutable annotations to inner-object format templates.
include/xrpl/protocol/detail/ledger_entries.macro Annotates ledger entry field templates with soeCONSTANT / soeNOTCONSTANT.
include/xrpl/protocol/detail/features.macro Removes an outdated comment line (no behavioral change).
include/xrpl/protocol/SOTemplate.h Introduces SOEConstant and extends SOElement constructors/accessors to carry mutability metadata.

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

Comment thread src/libxrpl/tx/invariants/InvariantCheck.cpp Outdated
Comment thread src/libxrpl/tx/invariants/InvariantCheck.cpp Outdated
Comment thread src/libxrpl/tx/invariants/InvariantCheck.cpp Outdated
Comment thread include/xrpl/protocol/SOTemplate.h Outdated
@codecov

codecov Bot commented Apr 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.54545% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.9%. Comparing base (ff02269) to head (ca635c0).
⚠️ Report is 23 commits behind head on develop.

Files with missing lines Patch % Lines
src/libxrpl/tx/invariants/InvariantCheck.cpp 92.7% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #7029     +/-   ##
=========================================
- Coverage     82.0%   81.9%   -0.0%     
=========================================
  Files         1007    1007             
  Lines        76873   76913     +40     
  Branches      8973    9002     +29     
=========================================
+ Hits         63012   63013      +1     
- Misses       13852   13891     +39     
  Partials         9       9             
Files with missing lines Coverage Δ
include/xrpl/protocol/SOTemplate.h 97.6% <100.0%> (+1.0%) ⬆️
include/xrpl/protocol/STObject.h 94.6% <ø> (ø)
include/xrpl/protocol/detail/ledger_entries.macro 100.0% <ø> (ø)
...ude/xrpl/protocol_autogen/LedgerEntryBuilderBase.h 100.0% <ø> (ø)
...ude/xrpl/protocol_autogen/TransactionBuilderBase.h 100.0% <ø> (ø)
include/xrpl/tx/invariants/InvariantCheck.h 100.0% <ø> (ø)
src/libxrpl/protocol/InnerObjectFormats.cpp 100.0% <100.0%> (ø)
src/libxrpl/protocol/LedgerFormats.cpp 100.0% <ø> (ø)
src/libxrpl/protocol/STTakesAsset.cpp 100.0% <ø> (ø)
...ibxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp 97.4% <ø> (ø)
... and 1 more

... and 6 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.

@mvadari mvadari changed the title feature: Add invariant checks to ensure only fields that are expected to be modified are modified feat: Add invariant checks to ensure only fields that are expected to be modified are modified Apr 27, 2026
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 27, 2026 13:33
xrplf-ai-reviewer[bot]

This comment was marked as resolved.

This comment was marked as resolved.

@mvadari mvadari force-pushed the mvadari/constant-field-invariant branch from 885c88c to 7de9560 Compare April 27, 2026 13:41
xrplf-ai-reviewer[bot]

This comment was marked as resolved.

@Tapanito

This comment has been minimized.

@mvadari

This comment has been minimized.

@github-actions

This comment has been minimized.

@godexsoft

This comment has been minimized.

Comment thread include/xrpl/protocol/SOTemplate.h
Comment on lines +971 to +974
// Optional constant fields may be added or removed,
// but their value must not change once present.
if (bPresent && aPresent && *bField != *aField)
return true;

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.

Can they, though? I would think that if an object is created without field X being set, it should stay unset forever. And the other way around, too.

Are there any cases where we expect a constant optional field to be set or cleared? If so, maybe we need another option - soeCONSTANTVALUE or something. Depending on how deep this goes, maybe soeCONSTANTVALUESETTABLE, soeCONSTANTVALUECLEARABLE, soeCONSTANTVALUEONLY.

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.

We have just this use-case in the SAV: #7077. Where a field is either set at creation or it is not, it can never be modified in any other way.

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.

Should I ignore whether or not the field is optional, then, and just always assume? I've made that change for now

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.

I added an SoeImmutableSetOnce field (e.g. sfFirstNFTokenSequence)

}
// soeNOTCONSTANT fields may change freely — no recursion
// into inner objects/arrays is needed because the parent
// field explicitly allows changes to its entire contents.

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.

Would it be possible to have a mutable parent field holding constant subfields?

Specifically, I'm thinking of SignerList.SignerEntries, which is mutable, but the SignerEntry inner object can't let the Account field change. That seems to require recursion. (Sorry.)

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.

I explored that at first but it got complicated quickly, so I left it out for now (we can revisit later)

Comment thread src/libxrpl/tx/invariants/InvariantCheck.cpp
Comment thread src/test/app/Invariants_test.cpp
Copilot AI review requested due to automatic review settings May 6, 2026 15:40
Comment thread src/libxrpl/protocol/InnerObjectFormats.cpp Outdated
Comment thread src/libxrpl/protocol/InnerObjectFormats.cpp Outdated

@Tapanito Tapanito left a comment

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.

The ledger_entries.macro and InnerObjectFormats annotations need to be audited very carefully.

We might not have unit-tests that are mutating some mutable fields, thus marking field that are technically mutable as immutable, would cause the invariant to fire.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

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

@github-actions

github-actions Bot commented Jun 8, 2026

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.

Two undocumented behaviour changes flagged inline — both are likely intentional but should be called out in the PR description.

Review by Claude Sonnet 4.6 · Prompt: V15

Comment thread src/libxrpl/tx/invariants/InvariantCheck.cpp
@mvadari

mvadari commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

The ledger_entries.macro and InnerObjectFormats annotations need to be audited very carefully.

We might not have unit-tests that are mutating some mutable fields, thus marking field that are technically mutable as immutable, would cause the invariant to fire.

I did a couple of audits on this logic to ensure that it was good.

@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.

Solid invariant improvements — nothing to flag.

Review by Claude Sonnet 4.6 · Prompt: V15

@Tapanito

Tapanito commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

@mvadari given that this invariant iterates through all fields of each SLE, what are the performance implications of this PR?

@github-actions

Copy link
Copy Markdown

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

@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.

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

Comment on lines 955 to 958
auto const* bField = before.peekAtPField(sf);
auto const* aField = after.peekAtPField(sf);
bool const bPresent = (bField != nullptr) && bField->getSType() != STI_NOTPRESENT;
bool const aPresent = (aField != nullptr) && aField->getSType() != STI_NOTPRESENT;

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.

What do you think about creating these as std::optional? So you don't need the separate *Present flags?

// It'll probably be easier with a helper
auto getField = [](STObject const& obj, auto const& sf)
	-> std::optional<STBase const*>
{
	auto const* field = obj.peekAfPField(sf);
	if (field == nullptr || field->getSType() == STI_NOTPRESENT)
		return std::nullopt;
	return field;
};

auto const bField = getField(before, sf);
auto const aField = getField(after, sf);

Comment on lines +993 to +996
// LedgerStateFix repairs ledger invariants, so it may need to modify
// fields that are otherwise immutable.
if (tx.getTxnType() == ttLEDGER_STATE_FIX)
return true;

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.

Any examples, or is this future-proofing?

return false;
}

continue;

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.

Suggestion: Add a comment

// If the template was used, the field-by-field check is redundant, so skip it.
continue;


// Template-based checks: SoeImmutableSetOnce field on Oracle.
{
Keylet oracleKeylet = keylet::amendments();

@ximinez ximinez Jun 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 could initialize oracleKeylet with bogus values

Keylet oracleKeylet{ltAny, beast::kZero};

(Edit: Because I was confused at first, and having a clearly bogus value will avoid that.)

@mvadari mvadari added this to the 3.4.0 milestone Jun 23, 2026

@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 Sonnet 4.6 · Prompt: V15

@github-actions

Copy link
Copy Markdown

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants