Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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>
// Canonical sysio.system emissions types + compute_epoch_emission. The
// [[sysio::contract("sysio.system")]] attribute on emission_config / t5_state
Expand Down Expand Up @@ -359,7 +360,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.
35 changes: 15 additions & 20 deletions contracts/sysio.opreg/src/sysio.opreg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,28 +345,23 @@ 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 two sites that can change it; see
/// `uwrit::lock_sum`.

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] Keep this on the three-path invariant. This says “two sites,” but the rollup is mutated at three: try_select_winner adds, chklocks decrements, and sweeplocks decrements after an upheld challenge. That third path was the source of the stale-cache bug fixed in this PR, so leaving the old count here undercuts the invariant documented in lock_sum. Please update this to say three sites and name them. The adjacent locks_t comment in sysio.uwrit.hpp also still says opreg::available() scans byunderwriter; it should identify locks as the authority and locksums as the O(1) read cache.

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

You are right on both, and the second one is the more useful catch: the locks_t comment was still describing the scan that locksums exists to replace, so the file taught the old model at the point a reader is most likely to look it up.

sum_locks_inline's comment now enumerates all three with direction, and states why the count matters at this site specifically — it is the cross-contract reader, it trusts the rollup completely, and a bucket left positive after its last row is gone suppresses that collateral permanently.

locks_t now names locks as the AUTHORITY and locksums as the O(1) read cache available() reads, with the reason the old scan does not scale: locks are held for the full wall-clock challenge window and are never released by delivery, so a bucket's live count is (settlement rate × lock duration).

I swept for the claim rather than fixing only the two sites you named, and three more had available() reading the lock rows:

  • the file header's opening bullet, which had opreg reading "this table via a mirror"
  • the lock_entry doc, which called the triple the indexing surface available() uses and pointed cross-contract at locks_t rather than locksums_t
  • the README's locks row ("consulted by sysio.opreg::available()") and its integration note — both contradicting the locksums row one line below, which already said the read is O(1) instead of a scan

Two adjacent bits of staleness turned up while there. The header bullet described the composite as one of "two secondary indexes", byuwck and byunderwriter — neither exists; the composite is deliberately not a table-managed index (the locks_t comment says so directly), and the uint64 indexes are byuw, byuwreq and byexpire. And lock_entry said its rows are erased by release, an action this contract does not have — the erase sites are chklocks and sweeplocks, which is also where the two sub_locked_total calls live.

Comments and markdown only. sysio.uwrit.wasm, sysio.opreg.wasm and both .abi files rebuild byte-identical, so this commit carries no artifact.

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.
5 changes: 3 additions & 2 deletions contracts/sysio.uwrit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ chain deregistered) refund in full.
|-------|----------|-------------|
| `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` |

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] Include sweeplocks in the COMPLETED transition. This row still defines COMPLETED only as the result of chklocks removing the final lock, but an UPHELD challenge calls sweeplocks, which erases the held locks and invokes the same finalize_settled_uwreqs tail before expiry. Update this row and the matching winner-selection comments in sysio.uwrit.cpp that still say locks are released only by chklocks, so all lifecycle documentation includes the early upheld-challenge path.

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 0902f94. You were right, and finalize_settled_uwreqs was already saying so from the inside — its own doc reads "Shared by chklocks (natural expiry) and sweeplocks (an UPHELD underwriter challenge's slash-sweep)". The function knew it had two callers; the lifecycle documentation around it did not.

I swept for the claim rather than fixing only this row, and there were four: the README's uwreqs row, try_select_winner's doc block, the inline comment at the lock push, and pruneuwreqs' note on why CONFIRMED never reaches it. A fifth, the collateral_lock_duration_ms field doc, was silent on the window being cut short and now says so.

Comments and markdown only — no __LINE__ or __FILE__ anywhere in the contract, so codegen is untouched and the committed artifacts stand.

Fuller write-up: #563 (comment)

| `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 |
| `locks` | `lock_entry` | Flat per-leg lock vector consulted by `sysio.opreg::available()`. 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 only by `try_select_winner` (on a win) and `chklocks` (on release); a bucket's row is erased once its total reaches zero, so an absent row reads as zero |
| `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,7 +78,7 @@ 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, at most `max_rows` per call (`advance` passes `MAX_LOCK_RELEASE_PER_EPOCH`); 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 |
| `sumlocks` | read-only | Sum an underwriter's active locks for a `(chain, token)` bucket — the lock half of `sysio.opreg::available()` |

Expand Down
124 changes: 113 additions & 11 deletions contracts/sysio.uwrit/include/sysio.uwrit/sysio.uwrit.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,28 @@ namespace sysio {
// subsequent epochs.
static constexpr uint32_t MAX_UWREQ_PRUNE_PER_EPOCH = 32;

// Locks `sysio.epoch::advance` passes to `chklocks` each epoch. Sized
// like MAX_UWREQ_PRUNE_PER_EPOCH / MAX_FWQ_DRAIN_PER_EPOCH, and for the
// same reason — but the exposure here is sharper than either, because
// lock EXPIRY is inherently bursty. Every lock is stamped
// `now + collateral_lock_duration_ms` at creation, so a burst of
// settlements inside one epoch produces a burst of expiries inside one
// epoch, exactly one lock-duration later. Sustained swap traffic
// therefore presents `chklocks` with a whole epoch's settlements at
// once, and per expired lock the sweep does an inline
// `opreg::releaselock` dispatch plus an erase — all inside advance's
// hard, uncatchable transaction CPU deadline.
//
// Unbounded, a large enough expiry burst aborts `advance`; and because
// those same locks are still expired at the next advance, it aborts
// identically every epoch thereafter — a PERMANENT chain-wide epoch
// stall rather than a transient one. Bounded, an oversized burst is
// just release latency that drains across subsequent epochs, which is
// harmless: the challenge window has already closed, so a lock freed an
// epoch or two late costs only a brief overstatement of the
// underwriter's reserved collateral.
static constexpr uint32_t MAX_LOCK_RELEASE_PER_EPOCH = 32;

// ── UWREQ row-growth rails (SEC-129 / WSA-223) ─────────────────────────
// Every uwreqs `modify` re-serializes the whole row inside the
// never-throw evalcons / advance dispatch surfaces, so per-field byte
Expand Down Expand Up @@ -346,8 +368,19 @@ namespace sysio {
///
/// This sweep is the ONLY lock-release path: locks are a wall-clock
/// challenge window (12h default) and are never released by delivery.
///
/// Budget-bounded, mirroring `pruneuwreqs` / `drainfwq`: walks the
/// `byexpire` index in ascending `expires_at_ms` and releases at most
/// `max_rows` locks (`max_rows == 0` is a no-op). Inlined from
/// `sysio.epoch::advance` with `MAX_LOCK_RELEASE_PER_EPOCH`; also
/// invocable by `sysio.uwrit` itself with a caller-chosen budget for a
/// manual backlog drain. Ascending-expiry order makes the bound a FIFO
/// drain — the oldest locks always release first, so no lock can starve
/// behind a sustained burst. NEVER throws past the auth gate: it runs
/// inline inside `advance`, where an abort stalls epoch progress
/// chain-wide.
[[sysio::action]]
void chklocks();
void chklocks(uint32_t max_rows);

/// Bounded UWREQ lifecycle sweep (SEC-129 / WSA-223). Inlined from
/// `sysio.epoch::advance` each epoch with `MAX_UWREQ_PRUNE_PER_EPOCH`;
Expand Down Expand Up @@ -454,6 +487,30 @@ namespace sysio {
/// a slash, the outpost routes seized collateral to that reserve via
/// `ReserveAmount`, even when multiple reserves exist for the same
/// `(chain_code, token_code)` pair.
/// The `(account, chain_code, token_code)` collateral-bucket digest:
/// the three uint64 identities packed little-endian into 24 bytes and
/// hashed. 3 × uint64 = 192 bits does not fit `uint128_t`, so the triple
/// is hashed to land in a `checksum256`.
///
/// SINGLE SOURCE for that encoding, and it must stay that way.
/// `lock_entry::by_underwriter_ck()` says which bucket a lock row
/// belongs to; `lock_sum_key::primary_key()` addresses that bucket's
/// materialized total. If the two derivations ever diverged, the rollup
/// would be keyed differently from the rows it summarizes and every
/// reader would silently observe zero locked — collateral already
/// committed to a live lock would look spendable. Both call this, so
/// they cannot diverge.
static checksum256 compose_account_chain_token_ck(name account,
sysio::slug_name chain_code,
sysio::slug_name token_code) {
std::array<uint8_t, 24> buf{};
uint64_t acc_v = account.value;
std::memcpy(buf.data() + 0, &acc_v, 8);
std::memcpy(buf.data() + 8, &chain_code.value, 8);
std::memcpy(buf.data() + 16, &token_code.value, 8);
return sysio::sha256(reinterpret_cast<const char*>(buf.data()), buf.size());
}

struct lock_key {
uint64_t lock_id;
uint64_t primary_key() const { return lock_id; }
Expand All @@ -476,17 +533,11 @@ namespace sysio {
/// `byexpire` so `chklocks` sweeps expired locks in ascending order.
uint64_t expires_at_ms = 0;

/// Composite checksum index for opreg's `available()` rollup:
/// `sha256(underwriter.value || chain_code.value || token_code.value)`
/// packed as 24 little-endian bytes. 3 × uint64 = 192 bits doesn't
/// fit `uint128_t`, so we hash the triple to land in `checksum256`.
/// Which collateral bucket this lock belongs to — see
/// `compose_account_chain_token_ck`, the single source of that
/// encoding, shared with `lock_sum_key::primary_key()`.
checksum256 by_underwriter_ck() const {
std::array<uint8_t, 24> buf{};
uint64_t uw_v = underwriter.value;
std::memcpy(buf.data() + 0, &uw_v, 8);
std::memcpy(buf.data() + 8, &chain_code.value, 8);
std::memcpy(buf.data() + 16, &token_code.value, 8);
return sysio::sha256(reinterpret_cast<const char*>(buf.data()), buf.size());
return compose_account_chain_token_ck(underwriter, chain_code, token_code);
}
/// Split-index for cheap per-operator scans (plan §B.2). Callers
/// pull all rows for a given underwriter and filter on
Expand Down Expand Up @@ -517,6 +568,57 @@ namespace sysio {
sysio::const_mem_fun<lock_entry, uint64_t, &lock_entry::by_expires_at_ms>>
>;

/// Primary key of `locksums`: one (underwriter, chain_code, token_code)
/// collateral bucket, addressed by the SAME digest
/// `lock_entry::by_underwriter_ck()` uses to say which bucket a lock row
/// belongs to — both call `compose_account_chain_token_ck`.
struct lock_sum_key {
name underwriter;
sysio::slug_name chain_code;
sysio::slug_name token_code;
checksum256 primary_key() const {

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.

[P2] Use one shared bucket-key helper. lock_sum_key::primary_key() duplicates lock_entry::by_underwriter_ck() byte-for-byte, even though the rollup's correctness depends on both encodings remaining identical. Extract the 24-byte hash derivation into a shared helper and call it from both sites; otherwise a later change can silently separate lock rows from the cache bucket used by the readers.

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 — and it was worse than it looked: there were three copies of that derivation, not two. Alongside lock_entry::by_underwriter_ck() and lock_sum_key::primary_key(), sysio.uwrit.cpp carried a private compose_account_chain_token_ck() with the same 24-byte packing and zero callers — vestigial from the v6 split-index change, kept "for any caller that still needs to derive the same key".

Rather than coin a new name I promoted that existing one into the header as the single source, since it is already this repo's name for the concept:

static checksum256 compose_account_chain_token_ck(name account,
                                                  sysio::slug_name chain_code,
                                                  sysio::slug_name token_code);

by_underwriter_ck() and lock_sum_key::primary_key() are now one-line calls to it, and the dead .cpp copy is deleted. Its doc comment carries the consequence you identified, so the next person to touch it sees why it is shared: if the two derivations diverge, the rollup is keyed differently from the rows it summarizes and every reader silently observes zero locked — collateral committed to a live lock looks spendable.

Your framing is the better one and I have adopted it. My original comment said the encodings "must not diverge", which is a note asking a future reader to be careful; making them one function means they cannot. Contracts unit suite re-run green after the change.

return compose_account_chain_token_ck(underwriter, chain_code, token_code);
}
SYSLIB_SERIALIZE(lock_sum_key, (underwriter)(chain_code)(token_code))
};

/// Materialized Σ `lock_entry.amount` for one (underwriter, chain_code,
/// token_code) bucket — the "locked" half of `sysio.opreg::available()`.
///
/// A CACHE of the `locks` table with exactly ONE writer: the only two

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] Include sweeplocks in the rollup invariant. After merging WIRE-297, sweeplocks is a second erase/decrement path outside chklocks, so the “only two paths” / “sole erase path” claim is now false—the exact omission that caused the previous rollup bug. Update this block, and the matching README/PR description, to list all three mutation sites: additions in try_select_winner, and decrements in chklocks and sweeplocks.

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 6b31e07.

You are right, and the framing is the part worth keeping: that stale sentence is not merely inaccurate, it is what made the omission easy. The block asserted chklocks was the sole erase path, so sweeplocks could be added without anything prompting the author to check the rollup — and the previous commit is that bug.

The lock_sum block now enumerates all three with their direction — try_select_winner ADDS, chklocks DECREMENTS, sweeplocks DECREMENTS — and records the consequence rather than just the count, so the next erase path added meets an argument instead of a list: the rollup is authoritative for available(), so a bucket left positive after its last row is gone suppresses that collateral permanently, because the rows that would decrement it are already erased.

I swept for the same claim rather than fixing only the block you flagged, and two neighbours were false for the same reason:

  • the header's chklocks doc, "This sweep is the ONLY lock-release path"
  • the .cpp section banner, "chklocks — ... (the ONLY release path)"

Both now say the only HEALTHY release path, and point at sweeplocks as the other eraser.

README: the locksums row carries the same three-way enumeration. While there I found its actions table never gained holdlocks / freelocks / sweeplocks when WIRE-297 landed on master — so the row I was writing referenced an action the document did not define. Those three rows are added; that gap is pre-existing rather than something this PR introduced, but leaving it would have made the new text dangle.

PR description's "One writer" section updated to match.

All comment-level in compiled code: sysio.uwrit.wasm and sysio.opreg.wasm rebuild byte-identical, so this commit carries no artifact.

/// code paths that can change a bucket's total both live in this
/// contract — `try_select_winner` (one lock per required leg, on a win)
/// and `chklocks` (release at expiry, the sole erase path). A row is

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.

[P1] Update the rollup from sweeplocks too. Current master adds a third lock-erasure path: an upheld challenge calls sweeplocks, which erases every held lock outside chklocks. After the rebase, each of those erases must call sub_locked_total; otherwise locksums remains positive after the authoritative rows are gone, sumlocks is permanently wrong, and a terminated operator can be considered settled/pruned while the stale cache blocks collateral on a later registration. Extend the challenge uphold test to assert rollup == scanned locks and zero after the sweep.

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 0ff2bdcsweeplocks now calls sub_locked_total on each erase.

You are right that it is a third erasure path and therefore carries the rollup obligation independently of chklocks. I checked the whole file rather than just this site: there are exactly two locks.erase calls (chklocks and sweeplocks) against two add_locked_total calls in winner selection, and both erase sites now decrement. freelocks and holdlocks only flip challenge_id, so they leave the rollup alone correctly.

Your description of the consequence is what makes this worth more than a missing line: the damage is permanent and silent. locksums is authoritative for available(), so a bucket left positive after its last row is gone suppresses that collateral forever — nothing ever decrements it again, because the rows that would have are already erased. A re-registering operator then finds deposited collateral unusable with nothing holding it, and sumlocks reports phantom locked value with no lock to point at.

chkuwchal_uphold_slashes_and_returns_bond now asserts both halves you asked for, right after the locks are confirmed gone: the rollup equals an authoritative scan of the lock rows, and it is zero. I verified it is a real regression rather than a passing assertion — with the sub_locked_total removed it fails scan_lock_total == get_lock_sum with [0 != 198], the scan reading zero while the cache still holds the swept 198.

I also left a comment at the erase naming it as the third path and stating the invariant, since the next erasure path added will have the same obligation and nothing structural enforces it.

contracts_unit_test: 638 cases, *** No errors detected

/// erased when its total reaches zero, so an absent row reads as zero
/// and the table holds only live buckets.
///
/// It exists because the derivation it replaces does not scale. Both
/// `sum_locks_inline` rollups (here and in sysio.opreg) previously
/// walked every lock row an underwriter held, each documenting the
/// assumption that "per-underwriter lock counts are O(1)-ish so the scan
/// is cheap". That is false under sustained swap traffic: locks are held
/// for the full wall-clock challenge window
/// (`collateral_lock_duration_ms`, 12h default) and are NEVER released
/// by delivery, so a bucket's live lock count is
/// (settlement rate × lock duration) — unbounded within the window. The
/// scan ran per candidate inside `try_select_winner` (up to
/// MAX_UWREQ_CANDIDATES of them per uwreq), i.e. inside the same
/// consensus-dispatch CPU budget whose overrun stalls the chain.
///
/// `sumlocks` reads this rollup, so it stays the cheap external answer
/// to "how much of this bucket is locked"; the authoritative recompute
/// is the `locks` table itself, which the contract tests scan and
/// compare against this total.
struct [[sysio::table("locksums")]] lock_sum {
name underwriter;
sysio::slug_name chain_code;
sysio::slug_name token_code;
uint64_t amount = 0;
SYSLIB_SERIALIZE(lock_sum, (underwriter)(chain_code)(token_code)(amount))
};

using locksums_t = sysio::kv::table<"locksums"_n, lock_sum_key, lock_sum>;

/// Per-underwriter race entry inside an UWREQ row. Tracks when each
/// leg of a dual-COMMIT pair arrived so `try_select_winner` can
/// resolve the race deterministically. Each leg's COMMIT is an
Expand Down
Loading
Loading