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
7 changes: 4 additions & 3 deletions contracts/sysio.epoch/src/sysio.epoch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
#include <sysio.opp.common/opp_keys.hpp>
#include <sysio.authex/sysio.authex.hpp>
#include <sysio.token/sysio.token.hpp>
// For uwrit::MAX_UWREQ_PRUNE_PER_EPOCH — the per-epoch budget advance hands
// to the inline `pruneuwreqs` sweep (the constant is owned by sysio.uwrit).
// For uwrit::MAX_LOCK_RELEASE_PER_EPOCH and uwrit::MAX_UWREQ_PRUNE_PER_EPOCH —
// the per-epoch budgets advance hands to the inline `chklocks` and
// `pruneuwreqs` sweeps (both constants are owned by sysio.uwrit).
#include <sysio.uwrit/sysio.uwrit.hpp>
// For reserve::MAX_CLAIM_SWEEP_PER_EPOCH — the per-epoch budget advance hands
// to the inline `sweepclaims` retention sweep (the constant is owned by
Expand Down Expand Up @@ -414,7 +415,7 @@ void epoch::advance() {
permission_level{get_self(), "owner"_n},
UWRIT_ACCOUNT,
"chklocks"_n,
std::make_tuple()
std::make_tuple(uwrit::MAX_LOCK_RELEASE_PER_EPOCH)
).send();

// Bounded UWREQ lifecycle sweep (SEC-129 / WSA-223): erase terminal
Expand Down
Binary file modified contracts/sysio.epoch/sysio.epoch.wasm
Binary file not shown.
45 changes: 25 additions & 20 deletions contracts/sysio.opreg/src/sysio.opreg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -370,28 +370,33 @@ void opreg::regoperator(name account,

namespace {

/// Sum the active locks on `sysio.uwrit::locks` for a given (op, chain, token).
/// Returns 0 if uwrit's locks table is empty or if the operator has no locks
/// on that chain/token.
/// The active lock total on `sysio.uwrit` for a given (op, chain, token) —
/// an O(1) read of uwrit's `locksums` rollup. Returns 0 when the operator
/// holds no live locks on that chain/token (the rollup erases a bucket's row
/// once it empties, so an absent row IS zero).
///
/// Per v6 plan §B.2 (split-index design): `sysio.uwrit::locks_t` exposes only
/// uint64 secondary indexes. The `byuw` index keys on `underwriter.value`;
/// rows are filtered on `(chain_code, token_code)` in memory. Per-underwriter
/// lock counts are O(1)-ish in steady state so the scan is cheap.
/// This used to scan `sysio.uwrit::locks` through its `byuw` index and filter
/// `(chain_code, token_code)` in memory, on the stated assumption that
/// per-underwriter lock counts are "O(1)-ish in steady state so the scan is
/// cheap". That assumption does not hold: uwrit locks are held for the full
/// wall-clock challenge window and are never released by delivery, so a
/// bucket's live lock count is (settlement rate × lock duration). uwrit now
/// maintains the total at the THREE sites that can change it:
///
/// * `try_select_winner` — ADDS, one lock per required leg, on a win.
/// * `chklocks` — DECREMENTS, releasing locks at expiry.
/// * `sweeplocks` — DECREMENTS, erasing the held locks of a
/// commitment whose underwriter-fault challenge was
/// UPHELD (WIRE-297), outside `chklocks`.
///
/// That third path is the one this PR's stale-cache bug came from, so the
/// count is worth keeping exact here: this reader trusts the rollup
/// completely, and a bucket left positive after its last lock row is gone
/// suppresses that collateral forever. See `uwrit::lock_sum`.
uint64_t sum_locks_inline(name account, sysio::slug_name chain_code, sysio::slug_name token_code) {
uwrit::locks_t locks(opreg::UWRIT_ACCOUNT);
auto idx = locks.template get_index<"byuw"_n>();

uint64_t total = 0;
auto it = idx.lower_bound(account.value);
auto end = idx.upper_bound(account.value);
for (; it != end; ++it) {
if (it->chain_code != chain_code || it->token_code != token_code) continue;
// Saturating: amounts are uncapped uint64 (external-chain values); a
// wrapped subtotal would understate `reserved` and overstate availability.
total = opp::safe::add_sat_u64(total, it->amount);
}
return total;
uwrit::locksums_t sums(opreg::UWRIT_ACCOUNT);
uwrit::lock_sum_key pk{account, chain_code, token_code};
return sums.contains(pk) ? sums.get(pk).amount : 0;
}

/// Sum the pending (not-yet-flushed) withdraws on this contract for a given
Expand Down
Binary file modified contracts/sysio.opreg/sysio.opreg.wasm
Binary file not shown.
22 changes: 15 additions & 7 deletions contracts/sysio.uwrit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ rate every swap is charged.
against their WIRE account permissions, re-runs the LP variance check,
re-checks available collateral, then picks a winner.
- Writes one collateral lock per required leg and holds it for the full
wall-clock challenge window. **Locks are never released by delivery** — only
`chklocks` sweeps them once `expires_at_ms` passes.
wall-clock challenge window. **Locks are never released by delivery** — on the
healthy path only `chklocks` sweeps them, once `expires_at_ms` passes. An
UPHELD underwriter-fault challenge ends the window early instead: `sweeplocks`
erases that commitment's locks where they stand, which can be well before
`expires_at_ms`.
- Settles the winning swap against `sysio.reserv` and queues the outbound
`SWAP_REMIT`.
- Escrows and drains swap-from-WIRE requests (`fwqueue`), charging a revert fee
Expand Down Expand Up @@ -63,8 +66,9 @@ chain deregistered) refund in full.
| Table | Row type | Description |
|-------|----------|-------------|
| `uwconfig` | `uw_config` | Singleton: `fee_bps`, `collateral_lock_duration_ms`, `min_fromwire_amount`, `fromwire_revert_fee_bps`, `uwreq_pending_timeout_epochs`, `uwreq_retention_epochs` |
| `uwreqs` | `uw_request_t` | One row per swap intent — race state in `commits_by`, `winner`, lifecycle status, mirrored `variance_tolerance_bps`. Retained for `uwreq_retention_epochs` after ANY terminal transition — `COMPLETED` (after `chklocks` sweeps the final collateral lock; the reserve settlement itself already happened at winner selection, which is what made the row CONFIRMED), `REJECTED` (immediate failure via `reject_and_refund`), or `EXPIRED` (pending timeout, same path) — then erased by `pruneuwreqs` |
| `locks` | `lock_entry` | Flat per-leg lock vector consulted by `sysio.opreg::available()`. The `byexpire` secondary index lets `chklocks` sweep expired locks in one pass |
| `uwreqs` | `uw_request_t` | One row per swap intent — race state in `commits_by`, `winner`, lifecycle status, mirrored `variance_tolerance_bps`. Retained for `uwreq_retention_epochs` after ANY terminal transition — `COMPLETED` (once the last collateral lock is gone — normally when `chklocks` sweeps it at expiry, but earlier when `sweeplocks` erases it on an UPHELD challenge; both run the same `finalize_settled_uwreqs` tail. The reserve settlement itself already happened at winner selection, which is what made the row CONFIRMED), `REJECTED` (immediate failure via `reject_and_refund`), or `EXPIRED` (pending timeout, same path) — then erased by `pruneuwreqs` |
| `locks` | `lock_entry` | Flat per-leg lock vector — the AUTHORITY for locked collateral. `sysio.opreg::available()` does not scan it; it reads the `locksums` cache below. The `byexpire` secondary index lets `chklocks` sweep expired locks oldest-first, up to its per-epoch budget |
| `locksums` | `lock_sum` | Materialized Σ `lock_entry.amount` per `(underwriter, chain_code, token_code)` bucket — the "locked" half of `sysio.opreg::available()`, read O(1) instead of scanning `locks`. Written by exactly three paths, all in this contract: `try_select_winner` ADDS (on a win), `chklocks` DECREMENTS (healthy release at expiry), and `sweeplocks` DECREMENTS (erasing a commitment's held locks on an UPHELD challenge). A bucket's row is erased once its total reaches zero, so an absent row reads as zero. Any new erase path must decrement too — the rollup is authoritative for `available()`, so a bucket left positive after its last row is gone suppresses that collateral permanently |
| `fwqueue` | `fromwire_q` | Escrowed swap-from-WIRE requests awaiting drain. `byepoch` secondary index |
| `uwcounters` | `uw_counters` | Monotonic id allocators (uwreq ids, lock ids) |

Expand All @@ -77,8 +81,11 @@ chain deregistered) refund in full.
| `rcrdcommit` | `sysio.msgch` | Record an underwriter's per-leg `UNDERWRITE_INTENT_COMMIT` bytes; resolves the race once both legs are present |
| `swapfromwire` | `user` | Escrow WIRE and enqueue a swap-FROM-WIRE request |
| `drainfwq` | `sysio.epoch` or self | Drain the from-WIRE queue: settle what prices, revert the rest (charging the revert fee on caller-fault causes) |
| `chklocks` | `sysio.epoch` or self | Sweep collateral locks whose wall-clock window has expired |
| `chklocks` | `sysio.epoch` or self | Sweep collateral locks whose wall-clock window has expired, oldest-first, EXAMINING at most `max_rows` rows per call (`advance` passes `MAX_LOCK_RELEASE_PER_EPOCH`) — the budget counts rows examined rather than locks released, so held locks and the challenge pokes they generate are bounded by it too; an oversized expiry burst drains across later epochs rather than aborting `advance` |
| `pruneuwreqs` | `sysio.epoch` or self | Expire timed-out PENDING uwreqs and erase terminal rows past their retention window |
| `holdlocks` | `sysio.chalg` | Mark a commitment's winning locks as held by an OPEN underwriter-fault challenge (WIRE-297); held locks are skipped by `chklocks` instead of released |
| `freelocks` | `sysio.chalg` | Clear the hold after a REJECTED or LAPSED challenge, so the next `chklocks` releases the locks normally |
| `sweeplocks` | `sysio.chalg` | Erase a commitment's held locks after an UPHELD challenge — the underwriter is already SLASHED, so each `releaselock` takes its deferred-slash branch. Decrements `locksums` like `chklocks` does |

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.

[P3] Qualify the early upheld-challenge sweep. This new row documents that sweeplocks erases and debits held locks after an UPHELD challenge, potentially before expires_at_ms, but the Responsibility summary above still says every lock is held for the full window and only chklocks sweeps it after expiry. Qualify that summary as the normal healthy path and mention the early slash-sweep so the two sections describe the same lifecycle.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f3e23c2. The Responsibility summary now marks the chklocks route as the healthy path and names the early sweeplocks erase alongside it, so it and the actions table describe the same lifecycle.

While there I found the chklocks row in that table stale in the same way and fixed it too: it read as a bound on locks released, which is what the budget counted before this branch made it challenge-aware. It counts rows EXAMINED now.

Fuller write-up: #563 (comment)

| `sumlocks` | read-only | Sum an underwriter's active locks for a `(chain, token)` bucket — the lock half of `sysio.opreg::available()` |

## Dependencies
Expand All @@ -87,7 +94,8 @@ chain deregistered) refund in full.
/ `SWAP_REVERT` through it.
- Settles against `sysio.reserv` (`applyswap` / `applyfromwire` / `paywire` /
`refundwire`), forwarding the winning underwriter so the fee accrues to them.
- Reads collateral from `sysio.opreg`; its `locks` table is the lock half of
that contract's `available()` rollup.
- Reads collateral from `sysio.opreg`; its `locksums` rollup — the O(1) cache
over the authoritative `locks` table — is the lock half of that contract's
`available()`.
- Lock sweeping and queue draining are inlined from `sysio.epoch::advance`.
- The off-chain counterpart is `wire-sysio/plugins/underwriter_plugin/`.
Loading
Loading