Skip to content

Add Dynamic MPT (XLS-0094) workload - #89

Open
manasip-prog wants to merge 10 commits into
mainfrom
add-dynamic-mpt-workload
Open

Add Dynamic MPT (XLS-0094) workload#89
manasip-prog wants to merge 10 commits into
mainfrom
add-dynamic-mpt-workload

Conversation

@manasip-prog

Copy link
Copy Markdown
Collaborator

Adds a DynamicMPTSet workload exercising XLS-0094 (mutable MPT issuances), submitted on-ledger as MPTokenIssuanceSet.

Model — opt-in mutability

An MPT issuance is immutable by default; mutability is granted only when a create-time MutableFlags (tmfMPT*) bit declares a capability/field as mutable. A mutating MPTokenIssuanceSet then either:

  • enables a capability via a MutableFlags set-enable bit (each requires the matching CAN_ENABLE_* declared at create),
  • rewrites MPTokenMetadata (requires CAN_MUTATE_METADATA), or
  • sets TransferFee (requires CAN_MUTATE_TRANSFER_FEE and an already-enabled CanTransfer).

All three ride typed xrpl-py fields; only the oversize-metadata malformation needs submit_raw.

Setup cohorts

  • Mutable cohort (accounts 53–55): created with _DYNAMIC_MUTABLE_FLAGS declaring every capability enable-able + metadata/transfer-fee mutable — feeds the valid paths.
  • Immutable cohort (accounts 56–58): created with NO MutableFlags, so every later flag-enable / metadata / transfer-fee mutation fails with tecNO_PERMISSION — feeds a curated failure vector.

Both are seeded through the _run_phase setup infrastructure.

Faulty vectors

fuzz (generative), fake_issuance (tecOBJECT_NOT_FOUND), non_issuer (tecNO_PERMISSION), immutable_mutation (tecNO_PERMISSION), and oversize_metadata (temMALFORMED, via submit_raw since xrpl-py rejects the length at construction).

State tracking

MPTokenIssuance gains a mutable_flags field; _on_mpt_create populates it from the create tx's MutableFlags. ws_listener routes validated MPTokenIssuanceSet txns carrying MutableFlags / MPTokenMetadata / TransferFee into the synthetic DynamicMPTSet bucket.

Packaging

xrpl-py pinned to the pre-3.3-release-group branch (carries the XLS-0094 mutable-flag models the workload needs).

Verification

  • check-imports, check-endpoints, check-fuzz-coverage, check-modifier-coverage, check-assembler-roundtrip — all pass. DynamicMPTSet is auto-classified by every Modifier via _TX_NAMES.
  • tests/test_mpt_dynamic.py — 13 tests pass.

Testing note

The workload image relies on the target xrpld being built with XLS-0094 enabled. Run against staging/3.3.x from xrpld-private (per team guidance — not rippled/develop).


Co-authored by Augment Code


Pull Request opened by Augment Code with guidance from the PR author

@manasip-prog
manasip-prog marked this pull request as ready for review July 27, 2026 18:22

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

CLAUDE.md needs updating in the same change — setup chain, MPT cohorts section, synthetic-names list.

"/mpt/set/dynamic/random",
mpt_issuance_set_dynamic,
lambda w: (w.accounts, w.mpt_issuances, w.client),
None,

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.

Nothing updates tracked state when a set-enable lands. Flip CanTrade on-ledger and MPTokenIssuance.can_trade stays False — these issuances sit in the mpt_dex no-trade fault pool, so that curated tecNO_PERMISSION vector silently becomes a real trade. Add an updater on the real "MPTokenIssuanceSet" row (same trick as _on_payment_maybe_sponsored_account on "Payment") that ORs the set bits into the tracked flags.

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.

Done in be1033f. Added _on_mpt_issuance_set on the real "MPTokenIssuanceSet" REGISTRY row (same trick as _on_payment_maybe_sponsored_account on "Payment"): it ORs the TF_MPT_SET_CAN_TRADE/CAN_TRANSFER/REQUIRE_AUTH bits into the tracked flags and applies TF_MPT_LOCK/TF_MPT_UNLOCK, so an on-ledger CanTrade flip no longer leaves the issuance mis-classified in the mpt_dex no-trade fault pool. DynamicMPTSet stays None (STATE_UPDATERS is keyed by real type, so it rides this row); the XLS-82 lock/unlock Set path shares the same updater.

Comment thread workload/pyproject.toml

[dependency-groups]
dev = [
"pytest>=9.1.1",

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.

Nothing runs these tests — CI doesn't call pytest and neither does the pre-push list. Wire it into checks.yml and CLAUDE.md, or they'll rot.

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.

Done in be1033f. Added a unit tests step to .github/workflows/checks.yml (nix develop --command bash -c "cd workload && uv run pytest -q") so CI runs the suite alongside ruff/mypy/basedpyright, and documented uv run pytest in CLAUDE.md's checks block with a note that it runs in CI.

return [m for m in mpt_issuances if m.mutable_flags and m.issuer in accounts]


def _immutable_issuances(

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.

This matches every zero-mutable_flags issuance, including all the regular step-4 cohorts — so the dedicated 56–58 cohort only buys guaranteed existence. Drop it, or keep it and say why in the setup comment.

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.

This thread predates the Aug 24 rework to XLS-0094's opt-out ImmutableFlags model (the branch flipped from opt-in MutableFlags). _mutable_issuances no longer keys off a zero-flags condition that could match the regular cohorts — it now filters m.dynamic and not m.immutable_flags and m.issuer in accounts. The dynamic marker is stamped by setup only on the dedicated 53-58 cohort (setup.py after step 4b), so the regular step-4 cohorts [0..5] are structurally excluded and their DEX/AMM + XLS-82 flag state is never disturbed. test_mutable_issuances_excludes_immutable_unowned_and_nondynamic guards exactly this (a dynamic=False regular issuance is excluded).

Comment thread workload/src/workload/setup.py Outdated
_IMMUTABLE_MPT_RANGE = range(56, 59) # accounts[56..58]
# Opt-in create-time MutableFlags for the mutable cohort: declare every
# capability enable-able + metadata/transfer-fee mutable.
_DYNAMIC_MUTABLE_FLAGS = (

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.

All three issuances declare CAN_ENABLE_REQUIRE_AUTH, and enables are one-way (no clear bits in the pinned xrpl-py). Holders get these tokens in step 5 with no issuer-side auth, so the first RequireAuth flip locks them out permanently — over a long run all three converge to that and the cohort goes dark.

Split it: one blockable cohort that declares CAN_ENABLE_REQUIRE_AUTH (holders getting locked out is the point), one safe cohort without it (RequireAuth there = deterministic tecNO_PERMISSION, and its holders can never be blocked). Keeps valid traffic alive for the whole run.

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.

Addressed in be1033f, though the rework took a slightly different shape than the split-cohort suggestion. Under the opt-out model the mutable cohort (53-55) is created with NO ImmutableFlags, so every capability is enable-able — which reintroduces exactly the one-way RequireAuth-lockout you flagged. Rather than carve a separate blockable cohort, valid traffic now draws flag-enables from _VALID_SET_ENABLE_FLAGS (= _SET_ENABLE_FLAGS minus TF_MPT_SET_REQUIRE_AUTH), so valid paths can never latch RequireAuth and dark the cohort's seeded holders. RequireAuth-enable is still exercised destructively via the fuzz vector and the immutable-cohort faulty vector (frozen TIF_MPT_REQUIRE_AUTH -> tecNO_PERMISSION). Happy to split into a dedicated blockable cohort instead if you'd rather see the holders-locked-out path land on-ledger as a first-class valid outcome.

transfer_fee=params.mpt_transfer_fee(),
)
else:
# set-enable latch; re-enabling an already-set capability is a no-op tesSUCCESS.

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.

Six one-way latches × three issuances — everything's latched early in a run, then this arm is a permanent no-op tesSUCCESS. Weight the metadata/fee arms higher.

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.

Done in be1033f. The valid arm now weights the repeatable mutations 3x over the latch: choice(["metadata"] * 3 + ["transfer_fee"] * 3 + ["flag_enable"]). metadata/transfer_fee land fresh on the mutable cohort every time, so valid traffic stays meaningful after the capability latches saturate to no-op tesSUCCESS.

Comment thread workload/pyproject.toml
# core wheel this installs and is built separately in the Antithesis image; see
# scripts/setup-confidential-crypto.sh.
[tool.uv.sources]
xrpl-py = { git = "https://github.com/XRPLF/xrpl-py.git", branch = "pre-3.3-release-group" }

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.

XLS-0094 models now also hang off this moving branch pin, which already broke the build once (flag rename). Pin a rev — fine as a follow-up.

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.

Leaving as a follow-up per your note. The XLS-0094 models ride the same pre-3.3-release-group branch pin as the Confidential MPT (XLS-0096) and sponsor (XLS-68) models already on this branch, so pinning a rev is a single cross-cutting change for all of them rather than something specific to this PR. Tracked to pin the whole group once the pre-release branches converge upstream.


# tmfMPTSet* set-enable bits: which capability a mutation turns on. Each requires
# the matching TMF_MPT_CAN_ENABLE_* declared in the create-time MutableFlags.
_SET_ENABLE_FLAGS = [

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.

xrpl-py has seven set bits, this list has six — TMF_MPT_SET_CAN_HOLD_CONFIDENTIAL_BALANCE is missing (and the matching CAN_ENABLE at create). Deliberate? If so, comment why. If not, an issuance turning confidential-capable mid-run is worth having.

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.

Not deliberate — added in be1033f. _SET_ENABLE_FLAGS now carries all seven of xrpl-py's set bits, including TF_MPT_SET_CAN_HOLD_CONFIDENTIAL_BALANCE, so a mutable issuance can turn confidential-capable mid-run. Under the reworked opt-out model there's no matching create-time CAN_ENABLE to declare — the capability is enable-able unless frozen via ImmutableFlags. test_set_enable_flags_include_confidential_balance guards its presence.

DynamicMPTSet handler submitted as MPTokenIssuanceSet using the opt-in mutable_flags model from xrpl-py pre-3.3-release-group. Valid paths cover flag-enable, metadata, and transfer-fee mutations on a mutable cohort (accounts 53-55); faulty paths cover fake issuance, non-issuer, immutable-cohort (56-58) mutation, oversize metadata, and generative fuzz. Includes setup cohorts, state tracking of mutable_flags/can_transfer, ws_listener routing, registry/ticket wiring, driver script, and unit tests.
@manasip-prog
manasip-prog force-pushed the add-dynamic-mpt-workload branch from e933016 to 700b046 Compare August 3, 2026 15:42
A second coverage catalog (workload/tests/test_*.py copied into
/opt/antithesis/catalog) flips the Antithesis SDK past its single-catalog
early-return into the grading path, which find_spec()s the workload
catalog's bare module names and raises ModuleNotFoundError on
'transactions.*' at startup (t~26s, before setup).

Exclude workload/tests/ and __pycache__ from the build context via
.dockerignore, and defensively rm them inside the catalog dir in
Dockerfile.workload in case the CI build context/ignore rules differ.
The confidential-crypto setup step cloned XRPL_PY_REF, whose default was
the deleted branch 'confidential-mpt', failing the build with
'Remote branch confidential-mpt not found'. Point it at
'pre-3.3-release-group' — the branch pinned in workload/pyproject.toml
[tool.uv.sources] and resolved by uv sync — whose MPT_CRYPTO_VERSION
(1.0.2) matches the rippled mpt-crypto pin, so the crypto now builds
instead of failing/skipping.
The pinned xrpl-py branch (pre-3.3-release-group @ c636721, matching rippled
rc5/rc6) inverted the XLS-0094 flag model from opt-in mutability
(MutableFlags/tmfMPT*) to opt-out immutability (ImmutableFlags/tifMPT*). The
removed MutableFlag identifiers crashed the workload at import during setup.

- mpt_dynamic.py: enable capabilities via TF_MPT_SET_* bits in the Flags field
  (MPTokenIssuanceSetFlag); drop the create-time mutate gates; gate mutable vs
  immutable cohorts on the new immutable_flags model field + a dynamic marker.
- models.py: rename MPTokenIssuance.mutable_flags -> immutable_flags (inverted
  semantics) and add a dynamic cohort marker.
- setup.py: mutable cohort sends no ImmutableFlags (fully mutable); immutable
  cohort freezes every TIF_* bit; mark the [53..58] cohorts dynamic so the
  handlers never touch the regular MPT cohorts [0..5].
- transactions/__init__.py: _on_mpt_create reads ImmutableFlags.
- ws_listener.py: route DynamicMPTSet on capability set-enable Flags bits
  (mask 0x1FC), MPTokenMetadata, or TransferFee.
Antithesis run #1440 flagged workload::always : meta_matches_tx_type
failing with tx_type=DIDSet, expected=[Created,Modified,DID]. A DIDSet
re-applying unchanged URI/Data/DIDDocument on an existing DID is a
legitimate tesSUCCESS, but rippled (DID.cpp calls view().update()
unconditionally) then has ApplyStateTable drop the byte-identical
ModifiedNode, leaving no DID node in AffectedNodes. DIDSet has no per-tx
field that distinguishes a create from an update, so the
_IDEMPOTENT_UPDATE_MARKER trick cannot exempt it. Drop the row (mirrors
the SponsorshipSet decision); DIDDelete stays since it always Deletes.

Latent bug unmasked once setup completes past the DynamicMPT phase and
more DIDSet txns fire against accounts already holding a DID.
Antithesis run #1440 flagged "Always: Commands finish with zero exit code
-> parallel_driver_mpt_set_dynamic_random.sh" (exit 101, ~9ms, empty
stdout/stderr). The driver was committed with mode 100644 (-rw-r--r--)
while every other parallel_driver_*.sh is 100755; the test-composer failed
to exec the non-executable file before curl ever ran. Restore the +x bit
in the git index (blob unchanged) so the image ships it executable.
EscrowCancel never reached tesSUCCESS (always tecNO_TARGET): EscrowFinish
raced and won because its window opens before CancelAfter, deleting every
tracked escrow before it could mature.

- Add a 'cancel_designated' EscrowCreate flavour: a conditional escrow with
  a near-term CancelAfter whose fulfillment is intentionally dropped, so it
  is structurally unfinishable by the workload and survives to CancelAfter
  maturity for EscrowCancel to consume. fix1571 forbids a CancelAfter-only
  escrow, so it carries a Condition.
- Restrict _escrow_finish_base to escrows we can actually construct a finish
  for (no condition, or fulfillment retained), returning None otherwise. Also
  fixes a latent crash where a conditional escrow lacking a retained
  fulfillment (e.g. ws-listener re-add) hit xrpl-py's EscrowFinish validation.
… pytest CI, docs

- __init__.py: add _on_mpt_issuance_set updater on the real MPTokenIssuanceSet
  row so a validated set-enable (CanTrade/CanTransfer/RequireAuth) + lock/unlock
  ORs into tracked MPTokenIssuance flags (DynamicMPTSet rides it; keeps the
  mpt_dex/AMM cohort classification honest).
- mpt_dynamic.py: add the 7th set-enable bit (CAN_HOLD_CONFIDENTIAL_BALANCE);
  add _VALID_SET_ENABLE_FLAGS (excludes REQUIRE_AUTH) so valid traffic never
  one-way-latches holders out; weight metadata/transfer_fee 3x over flag_enable.
- checks.yml: run uv run pytest in CI.
- CLAUDE.md: document the Dynamic MPT setup step 4b, cohorts, DynamicMPTSet
  synthetic name, and the pytest gate.
- test_mpt_dynamic.py: cover the valid-pool RequireAuth exclusion + 7th bit.
@manasip-prog

Copy link
Copy Markdown
Collaborator Author

@vvysokikh1 all seven inline threads addressed in be1033f (replied inline on each). Summary:

  • State tracking_on_mpt_issuance_set on the real MPTokenIssuanceSet row ORs set-enable bits + lock/unlock into tracked flags.
  • pytest in CIunit tests step added to checks.yml; documented in CLAUDE.md.
  • _mutable_issuances scope — obsoleted by the opt-out rework; now gated on the dynamic cohort marker so regular cohorts [0..5] are untouched.
  • RequireAuth lockout — valid flag-enables draw from _VALID_SET_ENABLE_FLAGS (RequireAuth excluded); still exercised destructively via fuzz + immutable cohort.
  • Weighting — metadata/transfer_fee weighted 3x over the flag-enable latch.
  • 7th set bitTF_MPT_SET_CAN_HOLD_CONFIDENTIAL_BALANCE added.
  • Branch pin — left as a follow-up per your note (shared with the XLS-0096/XLS-68 model pins).

CLAUDE.md docs (your top-level ask): added a Dynamic MPT cohorts (XLS-0094) section, the step-4b setup-chain entry, the DynamicMPTSet synthetic-name entry, and the uv run pytest gate.

Note several threads predate the Aug 24 rework from opt-in MutableFlags to XLS-0094's opt-out ImmutableFlags model, so their diff hunks show the old code. Re-requesting review.

@vvysokikh1

Copy link
Copy Markdown
Contributor

@manasip-prog hey, I’m no longer ripple’s employee so I won’t be able to help with this

@manasip-prog
manasip-prog force-pushed the add-dynamic-mpt-workload branch from fce0249 to be1033f Compare August 25, 2026 19:02
@lmaisons

Copy link
Copy Markdown
Collaborator

I'm having trouble reviewing this as a single unit. Would you mind splitting it up? The escrow rework, the DIDSet assertion fix, and the pin bump appear to each be independent changes, and having them separate would make it easier to reason about correctness and keep regressions bisectable.

For the dynamic MPT workload itself - a couple of things: the ws_listener routing looks like it'll fire for any MPTokenIssuanceSet carrying metadata, not just the dynamic cohort's issuances. Also, it looks like the PR description need an update: it references opt-in MutableFlags model but the code is doing opt-out with ImmutableFlags.

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.

3 participants