Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/check-endpoints
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ try:
'/mpt/create/random',
'/mpt/authorize/random',
'/mpt/set/random',
'/mpt/set/dynamic/random',
'/mpt/destroy/random',
'/loan/broker/set/random',
'/loan/broker/delete/random',
Expand Down
1 change: 1 addition & 0 deletions scripts/check-imports
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ from workload.transactions.deposit_preauth import deposit_preauth
from workload.transactions.signer_list_set import signer_list_set
from workload.transactions.did import did_set, did_delete
from workload.transactions.mpt import mpt_create, mpt_authorize, mpt_issuance_set, mpt_destroy
from workload.transactions.mpt_dynamic import mpt_issuance_set_dynamic
from workload.transactions.nft import nftoken_mint, nftoken_modify
from workload.transactions.offers import offer_create, offer_cancel
from workload.transactions.permissioned_dex import offer_create_domain, offer_create_hybrid, payment_domain, payment_domain_xc
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

curl --silent http://workload:8000/mpt/set/dynamic/random
14 changes: 10 additions & 4 deletions workload/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ warn_unused_ignores = true
# complementary flow/annotation checker, so third-party types resolve as Any here.
ignore_missing_imports = true

# Confidential MPT (XLS-0096), sponsor (XLS-68), and the rest of the models come from
# xrpl-py's pre-3.3-release-group branch (converges the pre-release WIP branches; not yet
# released). Proof generation (xrpl/ext/confidential) is EXCLUDED from the core wheel this
# installs and is built separately in the Antithesis image; see scripts/setup-confidential-crypto.sh.
# Confidential MPT (XLS-0096), Dynamic MPT (XLS-0094), sponsor (XLS-68), and the rest of
# the models come from xrpl-py's pre-3.3-release-group branch (converges the pre-release WIP
# branches; not yet released). Proof generation (xrpl/ext/confidential) is EXCLUDED from the
# 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.


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

]
4 changes: 4 additions & 0 deletions workload/src/workload/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ class MPTokenIssuance:
can_transfer: bool = False
require_auth: bool = False
locked: bool = False
# XLS-0094: create-time MutableFlags bitmask (tmfMPT*, opt-in model); 0 = fully
# immutable issuance. A set bit declares that a flag/field may later be
# enabled/mutated via MPTokenIssuanceSet.
mutable_flags: int = 0
holders: set[str] = field(default_factory=set)


Expand Down
5 changes: 5 additions & 0 deletions workload/src/workload/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ def mpt_metadata() -> str:
return bytes(randint(0, 255) for _ in range(length)).hex()


def mpt_transfer_fee() -> int:
"""1/10th basis points (1-50000 = 0.001-50%); kMaxTransferFee per spec."""
return randint(1, 50000)


# ── AMM ─────────────────────────────────────────────────────────────
def amm_trading_fee() -> int:
"""1/100,000th (0-1000 = 0-1%)."""
Expand Down
69 changes: 69 additions & 0 deletions workload/src/workload/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
MPTokenAuthorize,
MPTokenIssuanceCreate,
MPTokenIssuanceCreateFlag,
MPTokenIssuanceCreateMutableFlag,
MPTokenIssuanceSet,
MPTokenIssuanceSetFlag,
NFTokenCreateOffer,
Expand Down Expand Up @@ -99,6 +100,26 @@
_HOLDER_RANGE = range(62, 72) # accounts[62..71]
_VAULT_RANGE = range(10, 17) # accounts[10..16]

# Dynamic MPT (XLS-0094): opt-in mutable cohort created with create-time
# MutableFlags declaring every capability as enable-able + metadata/fee mutable,
# on indices unused elsewhere.
_DYNAMIC_MPT_RANGE = range(53, 56) # accounts[53..55]
# Immutable cohort: created with NO MutableFlags, so every later flag-enable /
# metadata / transfer-fee mutation must fail with tecNO_PERMISSION.
_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.

MPTokenIssuanceCreateMutableFlag.TMF_MPT_CAN_ENABLE_CAN_LOCK
| MPTokenIssuanceCreateMutableFlag.TMF_MPT_CAN_ENABLE_REQUIRE_AUTH
| MPTokenIssuanceCreateMutableFlag.TMF_MPT_CAN_ENABLE_CAN_ESCROW
| MPTokenIssuanceCreateMutableFlag.TMF_MPT_CAN_ENABLE_CAN_TRADE
| MPTokenIssuanceCreateMutableFlag.TMF_MPT_CAN_ENABLE_CAN_TRANSFER
| MPTokenIssuanceCreateMutableFlag.TMF_MPT_CAN_ENABLE_CAN_CLAWBACK
| MPTokenIssuanceCreateMutableFlag.TMF_MPT_CAN_MUTATE_METADATA
| MPTokenIssuanceCreateMutableFlag.TMF_MPT_CAN_MUTATE_TRANSFER_FEE
)

# Confidential MPT (XLS-0096): issuers + holders on indices unused elsewhere.
_CONF_ISSUER_RANGE = range(7, 9) # accounts[7..8]
_CONF_HOLDER_RANGE = range(72, 77) # accounts[72..76]
Expand Down Expand Up @@ -967,6 +988,8 @@ async def run_setup(workload: Workload) -> dict[str, int]:
| set(range(30, 36))
| set(range(40, 43))
| set(range(50, 53))
| set(_DYNAMIC_MPT_RANGE)
| set(_IMMUTABLE_MPT_RANGE)
| set(range(60, 72))
| set(range(72, 77))
# Cross-resource pool (Phase 4): rich accts + their delegates/sponsors must
Expand Down Expand Up @@ -1093,6 +1116,52 @@ async def run_setup(workload: Workload) -> dict[str, int]:
_mpt_issuance_exists,
)

# ── 4b. Dynamic MPT (XLS-0094) cohorts ──────────────────────────
# XLS-0094 is opt-in: an issuance is immutable unless a create-time
# MutableFlags (tmfMPT*) bit declares a capability/field as mutable. The
# mutable cohort (53-55) declares every CAN_ENABLE_* bit plus
# CAN_MUTATE_METADATA/CAN_MUTATE_TRANSFER_FEE, so every later flag-enable /
# metadata / TransferFee mutation passes preclaim; base CanTransfer lets
# fee>0 mutations stick and CanLock keeps the regular lock/unlock Set path
# reachable on this cohort. The immutable cohort (56-58) sends NO MutableFlags
# so every mutation reliably draws tecNO_PERMISSION.
summary["dynamic_mpt_issuances"] = await _run_phase(
workload,
"dynamic_mpt",
[
(
"MPTokenIssuanceCreate",
MPTokenIssuanceCreate(
account=accs[i].address,
flags=_LOCK | _XFER,
mutable_flags=_DYNAMIC_MUTABLE_FLAGS,
),
accs[i].wallet,
)
for i in _DYNAMIC_MPT_RANGE
if i < len(accs)
],
_mpt_issuance_exists,
)

summary["immutable_mpt_issuances"] = await _run_phase(
workload,
"immutable_mpt",
[
(
"MPTokenIssuanceCreate",
MPTokenIssuanceCreate(
account=accs[i].address,
flags=_LOCK | _XFER,
),
accs[i].wallet,
)
for i in _IMMUTABLE_MPT_RANGE
if i < len(accs)
],
_mpt_issuance_exists,
)

# ── 5. MPT authorization: holders authorize for each issuance ────
mpt_auth_txns = []
for mpt in workload.mpt_issuances:
Expand Down
22 changes: 21 additions & 1 deletion workload/src/workload/transactions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,14 @@
loan_pay,
loan_set,
)
from workload.transactions.mpt import mpt_authorize, mpt_create, mpt_destroy, mpt_issuance_set
from workload.transactions.mpt import (
mpt_authorize,
mpt_create,
mpt_destroy,
mpt_issuance_set,
)
from workload.transactions.mpt_dex import offer_create_mpt, payment_mpt
from workload.transactions.mpt_dynamic import mpt_issuance_set_dynamic
from workload.transactions.nft import (
nftoken_accept_offer,
nftoken_burn,
Expand Down Expand Up @@ -287,6 +293,9 @@ def _on_mpt_create(w: Workload, tx: dict, meta: dict) -> None:
require_auth=bool(flags & int(MPTokenIssuanceCreateFlag.TF_MPT_REQUIRE_AUTH)),
# lock state set later by setup, not at create
locked=False,
# XLS-0094: create-time MutableFlags (tmfMPT*, opt-in). 0 ⇒ fully
# immutable; a set bit declares a flag/field may later be enabled/mutated.
mutable_flags=int(tx.get("MutableFlags", 0) or 0),
)
w.mpt_issuances.append(issuance)

Expand Down Expand Up @@ -1098,6 +1107,17 @@ def _on_payment_maybe_sponsored_account(w: Workload, tx: dict, meta: dict) -> No
lambda w: (w.accounts, w.mpt_issuances, w.client),
None,
),
# DynamicMPTSet (XLS-0094): synthetic name; on-ledger type stays
# MPTokenIssuanceSet (mutation carries MutableFlags set-enable bits,
# MPTokenMetadata, or TransferFee). ws_listener fires tx_result for this
# bucket; no separate updater.
(
"DynamicMPTSet",
"/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.

),
(
"MPTokenIssuanceDestroy",
"/mpt/destroy/random",
Expand Down
Loading