Skip to content
Merged
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
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ Reserve/fee sponsorship of any supported tx now rides the submit-time **sponsor
### Sponsor-specific assertions (`assertions.py`, gated on `features.SPONSOR`)
`workload::always : no_sponsored_queue` — rippled's TxQ rejects only FEE-sponsored txns (`TxQ.cpp`: `sfSponsor && isFeeSponsored`), so a submit carrying `Sponsor` + `spfSponsorFee` must never yield `terQUEUED` — reserve-only sponsorship queues legitimately and is excluded; fired from `_assert_sponsor_submit_signals` (submit-side only — `ter*` never validates, so `tx_result`'s stream would silently miss it). Cross-type `sometimes` reachability signals ride the same submit-time hook plus `tx_result`'s validated side: `sponsor_fee_prefunded_used`/`sponsor_fee_cosigned_used` (validated `tesSUCCESS` + `Sponsor`+`spfSponsorFee`, split on `SponsorSignature` presence — needs validation, since "did the fee sponsor actually land" isn't certain pre-ledger), `sponsor_reserve_budget_exhausted` (`terNO_SPONSORSHIP` or `tecINSUFFICIENT_RESERVE` on a `Sponsor`-bearing submit), `sponsor_no_permission_seen` (`tecNO_SPONSOR_PERMISSION`, any tx), `sponsor_has_obligations_seen` (`AccountDelete` + `tecHAS_OBLIGATIONS`). The sponsor Modifier adds four more that replace the deleted per-type `Sponsored*` success/failure buckets (and their flakiness): `_assert_sponsor_reserve_usage` (validated, `tx_result`) fires `sponsor_reserve_succeeded` / `sponsor_reserve_failed` / `sponsor_reserve_exhausted` off any validated tx carrying `Sponsor`+`spfSponsorReserve` (a `tec` claims a fee and validates, so failures reach it); `sponsor_disallowed_type_rejected` fires submit-time from `_assert_sponsor_submit_signals` when a reserve sponsor rides a type outside `_RESERVE_SPONSOR_ALLOWED_TYPES` (mirror of rippled's `isReserveSponsorAllowed`) and gets `temINVALID_FLAG`. `SponsorshipSet` has no `_META_EXPECTATIONS` row: rippled keys the Sponsorship by account+sponsee with no ID field in the tx, so create and update are indistinguishable from the body, and a no-op update (e.g. a negative count delta clamping at an already-zero budget, or re-setting an already-set flag) drops the byte-identical modify from metadata — a legitimate tesSUCCESS with no Sponsorship node that the invariant can't tell from a regression. The `_IDEMPOTENT_UPDATE_MARKER` field trick needs a per-update field this type lacks, so the row is dropped rather than made unsound (`SponsorshipTransfer` likewise skipped — its target ledger-entry type varies between `Sponsorship`/account/object types).

### SponsorshipSet delta compat (`sponsorship_compat.py`) — TEMPORARY
xrpld 3.3.0-rc5 (xrpld-private #335) renamed the SponsorshipSet tx fields `sfFeeAmount`→`sfFeeAmountDelta` (Amount, nth 34) and `sfRemainingOwnerCount`→`sfRemainingOwnerCountDelta` (Int32, nth 2) with **delta** semantics: added to the existing object's value, must be positive on create, non-zero when present (`temBAD_AMOUNT`/`temINVALID`), all-fields-absent + no flags → `temREDUNDANT`, negative deltas clamp at zero on update. The pinned xrpl-py branch has neither the model fields nor the codec definitions, so `sponsorship_compat.py` injects the field headers into the live binarycodec maps at import and subclasses the model with `fee_amount_delta`/`remaining_owner_count_delta` — import `SponsorshipSet` from there, never from `xrpl.models`. Delete the shim (and revert imports) once xrpl-py's `pre-3.3-release-group` catches up; `_register_field` no-ops if the field appears upstream. The Sponsorship **ledger object** keeps absolute `FeeAmount`/`RemainingOwnerCount`, so meta-driven tracking (`_on_sponsorship_set`) needs no delta arithmetic. xrpl-py's codec can't encode negative XRP amounts, so the negative-delta fault vector rides the count field only.
### SponsorshipSet delta semantics
xrpld 3.3.0-rc5 (xrpld-private #335) renamed the SponsorshipSet tx fields `sfFeeAmount`→`sfFeeAmountDelta` (Amount, nth 34) and `sfRemainingOwnerCount`→`sfRemainingOwnerCountDelta` (Int32, nth 2) with **delta** semantics: added to the existing object's value, must be positive on create, non-zero when present (`temBAD_AMOUNT`/`temINVALID`), all-fields-absent + no flags → `temREDUNDANT`, negative deltas clamp at zero on update. Both the model fields (`fee_amount_delta`/`remaining_owner_count_delta`) and the codec definitions now ship upstream, so import `SponsorshipSet` from `xrpl.models` — the former `sponsorship_compat.py` shim that injected the field headers is deleted. The Sponsorship **ledger object** keeps absolute `FeeAmount`/`RemainingOwnerCount`, so meta-driven tracking (`_on_sponsorship_set`) needs no delta arithmetic. xrpl-py's codec special-cases delta fields to accept negative amounts (`binarycodec/types/amount.py`'s allow-list), so a negative `FeeAmountDelta` is encodable — the curated fault vector still rides the count field, leaving the negative-fee `temBAD_AMOUNT` path to generative fuzz.

### SponsorshipAudit (`sponsorship.py` + `app.py`)
Read-only `ledger_entry`/`account_info` cross-check of tracked sponsor state against the validated ledger — not a transaction, so no `REGISTRY` row (no `engine_result` to feed `seen`/`success`/`failure`, and `register_assertions()`'s reachability entry would starve waiting for a hit that structurally never comes). `app.py`'s `create_app()` wires `/sponsorship/audit/random` directly onto `_make_endpoint` (same `XRPLException`/timeout handling as REGISTRY rows) instead, invisible to `TX_TYPES`/the fuzz-coverage scan. Picks one random tracked `Sponsorship` and one random `w.sponsored_accounts` entry; a genuine `entryNotFound`/`actNotFound` against the validated ledger prunes the stale tracking entry (state legitimately drifts — deletes reaching us through a path the WS listener doesn't parse) rather than failing anything. Consistency is a `sometimes` (`sponsorship_audit_object_consistent`/`sponsorship_audit_account_consistent`), deliberately not an `always`: only a systematic break (the bucket never satisfying across a whole run) is worth triaging, and an `always` here would be flaky by construction.
Expand All @@ -126,13 +126,13 @@ No logger calls in `setup.py` or handlers — `send_event` + assertions cover ob
Fault-exposed validators (`val0`–`val4` + the isolated `fuzzer` validator) run `online_delete=256` + `ledger_history=256`; the tracking node `xrpld` stays `full`. Driven by `prepare-workload` `settings.node_config.online_delete` (0 disables), rendered per role by `xrpld.cfg.mako` (gated `is_validator and online_delete`) and unconditionally by `isolated_validator_xrpld.cfg.mako`; `main.py` passes the value into both. 256 is rippled's networked minimum and the only small legal value (`ledger_history ≤ online_delete`, enforced at startup). SHAMapStore rotates two NuDB backends every 256 validated ledgers: copies the live state map into the writable backend, drops the archive dir, prunes SQLite. `lastRotated` seeds to the first validated seq (~2), so rotation 1 lands ~ledger 258 and runs the risky path (state-map copy + `rotate()` archive-delete/`SavedState`-persist crash window). A full-history node keeps genesis forever, so its `complete_ledgers` lower bound never rises; `sidecar.py` reads each validator's `server_info` `complete_ledgers` and treats any rise above the first value seen as proof rotation pruned — emitting an `online_delete_rotation` event and firing the `sometimes` "online_delete rotation observed" (no threshold, reachable at rotation 1). It's a `sometimes` because a short run may never rotate. A crash inside `rotate()` can brick a node (`state database inconsistency` on restart) — that's a real rippled finding, not workload noise; don't mask it.

### Confidential MPT (XLS-0096)
Unconditionally on: the pinned `pre-3.3-release-group` xrpl-py carries the `ConfidentialMPT*` models and xrpld `develop` carries the `ConfidentialTransfer` amendment. `Dockerfile.workload` always runs the crypto build (`scripts/setup-confidential-crypto.sh`).
Unconditionally on: the pinned `main` xrpl-py carries the `ConfidentialMPT*` models and xrpld `develop` carries the `ConfidentialTransfer` amendment. `Dockerfile.workload` always runs the crypto build (`scripts/setup-confidential-crypto.sh`).

Five real on-ledger handlers (`transactions/confidential_mpt.py`: MergeInbox, Convert, Send, ConvertBack, Clawback) — true `ConfidentialMPT*` type, real-type `STATE_UPDATERS`, no synthetic-name mapping.

**Packaging.** Models come from xrpl-py's `pre-3.3-release-group` branch (git-pinned, in the core wheel; converges the pre-release sponsor + confidential WIP branches). Proof generation is `xrpl.ext.confidential` — the separate `xrpl-py-confidential` dist, EXCLUDED from the core wheel — so `uv sync` gets models but not proofs. `scripts/setup-confidential-crypto.sh` (in `Dockerfile.workload`) copies `xrpl/ext/confidential` into the venv, fetches `libmpt-crypto.so` from `XRPLF/mpt-crypto`'s public release, and compiles `_mpt_crypto` (fail-loud). Import is guarded: absent add-on → `CRYPTO_AVAILABLE=False`.
**Packaging.** Models come from xrpl-py's `main` branch (git-pinned, in the core wheel; carries all 3.3.0 features — the earlier `pre-3.3-release-group` pin predates that convergence). `Dockerfile.workload`'s `XRPL_PY_REF` must track the same ref so proofs and models agree. Proof generation is `xrpl.ext.confidential` — the separate `xrpl-py-confidential` dist, EXCLUDED from the core wheel — so `uv sync` gets models but not proofs. `scripts/setup-confidential-crypto.sh` (in `Dockerfile.workload`) copies `xrpl/ext/confidential` into the venv, fetches `libmpt-crypto.so` from `XRPLF/mpt-crypto`'s public release, and compiles `_mpt_crypto` (fail-loud). Import is guarded: absent add-on → `CRYPTO_AVAILABLE=False`.

**Version coherence (not hardcoded).** The script reads the target from the branch's `MPT_CRYPTO_VERSION` and cross-checks it against `RIPPLED_MPT_CRYPTO_VERSION` (`Dockerfile.workload` ARG) — the `mpt-crypto/*` pin of the **actual** xrpld the run builds. CI resolves that from the checked-out xrpld repo+ref (`start_experiment.yml`), NOT a hardcoded public fetch: the xrpld repo may be private (e.g. `XRPLF/xrpld-private staging/3.3.x-private`, which `raw.githubusercontent` can't read) and the ref isn't always `develop`. On divergence — or when the version is unset (a standalone build) — the crypto build is skipped (not failed): confidential valid paths go dark and a `.mpt_crypto_version_mismatch` marker fires `confidential_crypto_version_mismatch`. Currently `1.0.2`.
**Version coherence (not hardcoded).** The script reads the target from the branch's `MPT_CRYPTO_VERSION` and cross-checks it against `RIPPLED_MPT_CRYPTO_VERSION` (`Dockerfile.workload` ARG) — the `mpt-crypto/*` pin of the **actual** xrpld the run builds. CI resolves that from the checked-out xrpld repo+ref (`start_experiment.yml`), NOT a hardcoded public fetch: the xrpld repo may be private (e.g. `XRPLF/xrpld-private staging/3.3.x-private`, which `raw.githubusercontent` can't read) and the ref isn't always `develop`. On divergence — or when the version is unset (a standalone build) — the crypto build is skipped (not failed): confidential valid paths go dark and a `.mpt_crypto_version_mismatch` marker fires `confidential_crypto_version_mismatch`. The check is not raw string equality: `_COMPATIBLE_CLASSES` in the script lists releases whose library source is byte-identical, so bindings at one version still build against a rippled pinned to the other. xrpl-py `main` targets `1.0.4` while both xrpld `develop` and `staging/3.3.x-private` pin `1.0.2`, and `1.0.2..1.0.4` touches only CMake/packaging/tests (no `include/` or `src/` change) — hence that pair is listed. `1.0.5` adds a BSGS DLP solver, a real library change, so it is deliberately NOT equivalent; only extend the list after confirming `git diff <a> <b> -- include src` is empty.

`cc.CRYPTO_AVAILABLE` gates **valid** paths only. Faulty paths aren't gated: trivial-on-curve fixed-length blobs (66B ciphertext, 33B key, bogus proof) reach preclaim → `tecBAD_PROOF`/`temMALFORMED`/`tecOBJECT_NOT_FOUND` with no real crypto. Models validate the lengths rippled enforces, so `confidential_crypto.py` holds the wire-size constants and `params.py` builds faulty bases at them.

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.workload
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# on the xrpl-py binding version matching RIPPLED_MPT_CRYPTO_VERSION — the mpt-crypto
# pin of the ACTUAL xrpld the run uses, resolved by CI (the xrpld repo may be private).
# Empty (e.g. a standalone build) -> crypto skipped, not a build failure.
ARG XRPL_PY_REF=confidential-mpt
ARG XRPL_PY_REF=main
ARG RIPPLED_MPT_CRYPTO_VERSION=
RUN PYTHON=/opt/venv/bin/python bash scripts/setup-confidential-crypto.sh \
"$XRPL_PY_REF" "$RIPPLED_MPT_CRYPTO_VERSION"
Expand Down
1 change: 0 additions & 1 deletion scripts/check-imports
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ from workload.fuzz import submit_fuzzed, fuzz_mutate
from workload.rawfuzz import escalate
from workload.assembler import parse, reassemble
from workload.sequence import SequenceTracker
from workload.sponsorship_compat import SponsorshipSet
from workload.ws_listener import start_ws_listener
from workload.setup import run_setup
from workload.params import should_send_faulty, fake_account, fake_id
Expand Down
10 changes: 6 additions & 4 deletions workload/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ warn_unused_ignores = true
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.
# xrpl-py's main branch, which now carries all 3.3.0 features (the earlier
# pre-3.3-release-group pin predates that convergence). 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 — keep
# Dockerfile.workload's XRPL_PY_REF on the same ref so models and proofs agree.
[tool.uv.sources]
xrpl-py = { git = "https://github.com/XRPLF/xrpl-py.git", branch = "pre-3.3-release-group" }
xrpl-py = { git = "https://github.com/XRPLF/xrpl-py.git", branch = "main" }
32 changes: 28 additions & 4 deletions workload/scripts/setup-confidential-crypto.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# Build the XLS-0096 Confidential MPT crypto add-on into the workload venv.
# xrpl-py excludes proof generation (xrpl/ext/confidential) from its core wheel, so
# fetch it from the branch and compile its cffi extension in place. The pin the
# bindings target must equal the mpt-crypto version rippled was built against — a
# mismatch would have rippled reject every proof. That rippled version is passed in
# bindings target must be crypto-compatible with the mpt-crypto version rippled was
# built against — a mismatch would have rippled reject every proof. That rippled version is passed in
# ($2), resolved by the caller from the ACTUAL xrpld repo+ref used for the run: the
# xrpld repo may be private (raw.githubusercontent can't read it) and the ref is not
# always develop, so we no longer fetch a hardcoded public conanfile here. On
Expand Down Expand Up @@ -42,14 +42,38 @@ if [ -z "$RIPPLED_VERSION" ]; then
exit 0
fi

if [ "$version" != "$RIPPLED_VERSION" ]; then
# Releases whose library source is byte-identical, so bindings built against one
# emit proofs a rippled built against the other verifies. Only add a pair after
# confirming `git diff <a> <b> -- include src` on XRPLF/mpt-crypto is empty:
# 1.0.2..1.0.4 touches CMakeLists/BundleStatic/tests only (packaging + WASM CI),
# whereas 1.0.5 adds a BSGS DLP solver and is NOT equivalent to either.
_COMPATIBLE_CLASSES=("1.0.2 1.0.4")

# Same crypto? Exact match, or both in one equivalence class above.
crypto_compatible() {
[ "$1" = "$2" ] && return 0
local class
for class in "${_COMPATIBLE_CLASSES[@]}"; do
case " $class " in *" $1 "*)
case " $class " in *" $2 "*) return 0 ;; esac
;; esac
done
return 1
}

if ! crypto_compatible "$version" "$RIPPLED_VERSION"; then
echo "WARN: mpt-crypto divergence — xrpl-py($XRPL_PY_REF)=$version" \
"rippled=$RIPPLED_VERSION. Skipping confidential crypto build;" \
"confidential valid paths disabled (CRYPTO_AVAILABLE=False)." >&2
marker "xrpl-py=$version rippled=$RIPPLED_VERSION"
exit 0
fi
echo "mpt-crypto $version agreed (xrpl-py $XRPL_PY_REF == rippled $RIPPLED_VERSION)"
if [ "$version" = "$RIPPLED_VERSION" ]; then
echo "mpt-crypto $version agreed (xrpl-py $XRPL_PY_REF == rippled $RIPPLED_VERSION)"
else
echo "mpt-crypto $version (xrpl-py $XRPL_PY_REF) vs $RIPPLED_VERSION (rippled):" \
"same library source, proceeding."
fi

# ── Drop the ext source beside the installed core (xrpl.ext = PEP 420 namespace) ──
site="$("$PYTHON" -c 'import xrpl, os; print(os.path.dirname(os.path.dirname(xrpl.__file__)))')"
Expand Down
2 changes: 1 addition & 1 deletion workload/src/workload/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
NFTokenMintFlag,
Payment,
PermissionedDomainSet,
SponsorshipSet,
TicketCreate,
Transaction,
TrustSet,
Expand All @@ -62,7 +63,6 @@
from workload.assertions import assert_no_internal_error_submit
from workload.models import ConfidentialHolder, ConfidentialMPTIssuance, UserAccount
from workload.sequence import SequenceTracker
from workload.sponsorship_compat import SponsorshipSet
from workload.submit import submit_tx

# ── Constants ───────────────────────────────────────────────────────────
Expand Down
Loading