Add Dynamic MPT (XLS-0094) workload - #89
Conversation
vvysokikh1
left a comment
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| [dependency-groups] | ||
| dev = [ | ||
| "pytest>=9.1.1", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
| _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 = ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| # 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" } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = [ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
e933016 to
700b046
Compare
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.
|
@vvysokikh1 all seven inline threads addressed in be1033f (replied inline on each). Summary:
CLAUDE.md docs (your top-level ask): added a Note several threads predate the Aug 24 rework from opt-in |
|
@manasip-prog hey, I’m no longer ripple’s employee so I won’t be able to help with this |
fce0249 to
be1033f
Compare
|
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 |
Adds a
DynamicMPTSetworkload exercising XLS-0094 (mutable MPT issuances), submitted on-ledger asMPTokenIssuanceSet.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 mutatingMPTokenIssuanceSetthen either:MutableFlagsset-enable bit (each requires the matchingCAN_ENABLE_*declared at create),MPTokenMetadata(requiresCAN_MUTATE_METADATA), orTransferFee(requiresCAN_MUTATE_TRANSFER_FEEand an already-enabledCanTransfer).All three ride typed xrpl-py fields; only the oversize-metadata malformation needs
submit_raw.Setup cohorts
_DYNAMIC_MUTABLE_FLAGSdeclaring every capability enable-able + metadata/transfer-fee mutable — feeds the valid paths.MutableFlags, so every later flag-enable / metadata / transfer-fee mutation fails withtecNO_PERMISSION— feeds a curated failure vector.Both are seeded through the
_run_phasesetup infrastructure.Faulty vectors
fuzz(generative),fake_issuance(tecOBJECT_NOT_FOUND),non_issuer(tecNO_PERMISSION),immutable_mutation(tecNO_PERMISSION), andoversize_metadata(temMALFORMED, viasubmit_rawsince xrpl-py rejects the length at construction).State tracking
MPTokenIssuancegains amutable_flagsfield;_on_mpt_createpopulates it from the create tx'sMutableFlags.ws_listenerroutes validatedMPTokenIssuanceSettxns carryingMutableFlags/MPTokenMetadata/TransferFeeinto the syntheticDynamicMPTSetbucket.Packaging
xrpl-pypinned to thepre-3.3-release-groupbranch (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.DynamicMPTSetis 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
xrpldbeing built with XLS-0094 enabled. Run againststaging/3.3.xfromxrpld-private(per team guidance — notrippled/develop).Co-authored by Augment Code
Pull Request opened by Augment Code with guidance from the PR author