Skip to content

Add TransactionProposal invariants for Cosign - #8130

Open
ckeshava wants to merge 3 commits into
XRPLF:ripple/cosignfrom
ckeshava:cosign-invariants-v1
Open

Add TransactionProposal invariants for Cosign#8130
ckeshava wants to merge 3 commits into
XRPLF:ripple/cosignfrom
ckeshava:cosign-invariants-v1

Conversation

@ckeshava

@ckeshava ckeshava commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

High Level Overview of Change

Adds ValidTransactionProposal, a global invariant for the TransactionProposal ledger entry, plus tests. No behavior change for valid transactions.

The invariant enforces four properties:

  1. Immutable payload. ProposedTransaction may change only in its signature fields (SigningPubKey, TxnSignature, Signers, CounterpartySignature, SponsorSignature, BatchSigners, and nested Signers), plus PreviousTxn bookkeeping. SponsorshipTransfer may reassign the object's Sponsor and nothing else.
  2. Reserve accounting. Creating or deleting a proposal moves the owner's OwnerCount by that proposal's own reserve size (5, or 10 for a proposed Batch), and moves SponsoredOwnerCount / SponsoringOwnerCount consistently when the reserve is sponsored.
  3. Effect whitelist. Only transaction types implementing a proposal lifecycle operation may create, modify, or delete a proposal, with the exact counts the result allows. A failed TransactionProposalCreate must leave no trace; a successful one must create exactly one entry.
  4. Canonical signer arrays. Collected signer arrays stay sorted by account, free of duplicates, and within STTx::kMaxMultiSigners (32) and kMaxBatchSigners (24).

Context of Change

This is the invariant layer for the On-Chain Cosigner work (XLS-0103). Properties 1 and 4 restate spec sections 4.2.1 and 6.1; property 2 restates section 4.4.

It is implemented as a global invariant rather than in TransactionProposalCreate::visitInvariantEntry, because the properties are about the ledger entry itself and must hold no matter which transactor touches it. That is what makes property 3 meaningful: as TransactionProposalSign and TransactionProposalCancel land, each has to be added to the whitelist deliberately, and until then no other transaction type can manufacture or mutate a proposal.

Reserve reconciliation is applied only once a transaction has actually touched a proposal. Owner counts also move for offers, escrows, and for the ticket a transaction consumes to pay for itself, so the modelled deltas describe the ledger accurately only when a proposal is in play.

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)

The invariant is gated on featureCosign, so it cannot change behavior on a ledger where the amendment is not enabled.

Test Plan

Thirteen cases in Invariants_test.cpp. Eleven violate exactly one rule: a mutated TicketSequence; an insertion with no OwnerCount bump; an insertion from AccountSet; and eight signer-array cases covering duplicates, descending order, both length limits, an entry with no Account, and duplicates nested under CounterpartySignature, SponsorSignature, and a BatchSigner.

Two are negative controls asserting tesSUCCESS: a well-formed create, and a proposal left untouched. The first reuses the setup of the two failure cases above it and differs only in the OwnerCount bump and the transaction type, which pins each of those failures to the defect it names rather than to the hand-built ledger state.

The success path is also covered indirectly, since invariants run on every transaction in every test: the existing TransactionProposalCreate suite (17 cases, 1067 tests) exercises this invariant returning true, including sponsored create and sponsorship transfer.

@github-actions

Copy link
Copy Markdown

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

Introduce ValidTransactionProposal, a global invariant that enforces the
four properties a TransactionProposal ledger entry must hold:

- The proposed transaction is immutable except for its signature fields
  and PreviousTxn bookkeeping. SponsorshipTransfer may reassign the outer
  Sponsor field and nothing else.
- Owner reserve moves by the proposal's own reserve size when a proposal
  is created or deleted, including the sponsored and sponsoring counts
  when its reserve is sponsored.
- Only transaction types that implement a proposal lifecycle operation
  may create, modify, or delete a proposal, with the exact counts the
  transaction result allows.
- Collected signer arrays stay sorted, unique, and within their bounds.

Reserve reconciliation is applied only once a transaction has touched a
proposal, because owner counts also move for offers, escrows, and for
the ticket a transaction consumes to pay for itself.

Add thirteen invariant test cases: eleven that each violate one rule, and
two negative controls that reuse the failure cases' setup so every
failure is pinned to the defect it names.
The monolithic Invariants_test.cpp was split into per-topic files under
src/test/app/invariants/. Place the TransactionProposal cases in their own
file alongside the other feature suites.
@github-actions

Copy link
Copy Markdown

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

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.34641% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/libxrpl/tx/invariants/CosignerInvariant.cpp 99.3% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

The checked-in wrapper is stale, so check-autogen fails. This is
pre-existing on the base branch rather than caused by the invariant
work: XRPLF#8001 collapsed the transactions.macro settings into a TxSettings
struct, which renders an unset .privileges from a default that is now
enum-qualified. TransactionProposalCreate sets no .privileges, and the
merge that brought the refactor into this branch converted the macro
without re-running the generator.

Comment-only, and the output of:

  cmake --build build/codegen --target code_gen
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant