Skip to content

Uwrit: enforce UWREQ lifecycle expiry with bounded epoch-driven pruning#533

Open
heifner wants to merge 1 commit into
masterfrom
fix/sec-129
Open

Uwrit: enforce UWREQ lifecycle expiry with bounded epoch-driven pruning#533
heifner wants to merge 1 commit into
masterfrom
fix/sec-129

Conversation

@heifner

@heifner heifner commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

The underwriter contract declared a ten-epoch retention period and stamped expires_at_epoch on terminal UWREQ rows, but nothing ever read the field and no code path erased a row — and PENDING requests carried no deadline at all. Consensus state grew monotonically during normal operation, an abandoned race pinned the user's funds in custody indefinitely, and unbounded per-row payloads (full inbound attestation copy, verbatim per-candidate UIC blobs) meant every later modify — all of which run inside the never-throw evalcons/advance dispatch surfaces — rewrote an ever-larger row. Addresses SEC-129 / WSA-223.

  • New byexpire secondary index + pruneuwreqs(max_rows): a bounded sweep inlined from sysio.epoch::advance (after chklocks, before buildenv so expiry SWAP_REVERTs ride the same epoch's envelopes). PENDING rows past uwconfig.uwreq_pending_timeout_epochs are expired — refund/revert via the reject_and_refund machinery, terminal status UNDERWRITE_REQUEST_STATUS_EXPIRED (first producer of that proto value) — and terminal rows past uwconfig.uwreq_retention_epochs are erased. Auth mirrors chklocks/drainfwq (epoch or self; not permissionless — the sweep emits refunds); a backlog drains across epochs.
  • Deadline stamping is trigger-driven throughout: createuwreq/drainfwq stamp creation_epoch + pending_timeout; try_select_winner zeroes the deadline (the wall-clock lock window owns CONFIRMED rows — chklocks always terminalizes them); terminal transitions stamp the retention window. Zero means "no deadline, never swept". Nothing is scheduled; the sweep evaluates the field lazily when the epoch machinery fires it.
  • Payload compaction: non-winner UIC blobs clear at CONFIRMED; the inbound attestation copy and every stored UIC byte blob clear at terminal transitions — retention keeps compact audit metadata only.
  • Row-growth rails: per-leg UIC byte cap (2 KiB), candidate cap (32), source_tx_id/depositor/attestation-data caps at ingestion, and a projected pack_size whole-row guard before growing modifies — all fail-closed (log + skip), never check(), per the never-throw dispatch contract.
  • setconfig grows uwreq_pending_timeout_epochs + uwreq_retention_epochs (defaults 10/10; positive, ceiling-capped against deadline-stamp wrap).

ABI delta: new action pruneuwreqs, extended setconfig/uw_config, and the new uwreqs secondary index. Pre-launch dispensation applies to the uw_config singleton shape and to pre-upgrade rows lacking byexpire index entries — every environment rebuilds from genesis.

Validation

  • 13 new test cases; full contracts_unit_test suite green (520 cases). The dispatch-suite integration tests drive real epoch advances (the fixture now deploys sysio.system and satisfies the emissions gate, per the emissions_tests recipe) covering: pre-deadline advances leave rows untouched; PENDING → EXPIRED + compaction → retention → erased; from-WIRE expiry refunds the full escrow fee-exempt; the COMPLETED happy path ages out to steady state; 33 due rows against the 32-row budget prove the bound; the oversized-UIC and candidate-cap rails hold.
  • e2e gate green 13/13 across run 29942460590 (12/13; the swap-non-native-tokens failure was an outpost-side USDT provisioning revert, second-in-block behind a concurrent provisioning tx) + control run 29948649459 (same branch combination, that flow green — demonstrating the race is run-local and pre-existing; ticket to follow). Includes the 35-minute emissions soak: hundreds of advances with the inline sweep, no stall.

Companions: Wire-Network/wire-libraries-ts#53 (types regen) + Wire-Network/wire-tools-ts#38 (harness config); the gate was dispatched with all three branch overrides.

…ng (SEC-129 / WSA-223)

expires_at_epoch was recorded on terminal UWREQ rows but never read, and
no code path erased a row; PENDING requests had no deadline at all, so
consensus state grew monotonically and an abandoned race pinned the
user's funds in custody indefinitely.

- New byexpire secondary index + pruneuwreqs(max_rows): a bounded sweep
  inlined from sysio.epoch::advance (after chklocks, before buildenv so
  expiry SWAP_REVERTs ride the same epoch's envelopes). PENDING rows past
  uwconfig.uwreq_pending_timeout_epochs are expired -- refund/revert via
  the reject_and_refund machinery, terminal status EXPIRED (first
  producer of that proto value) -- and terminal rows past
  uwconfig.uwreq_retention_epochs are erased. Never throws past the auth
  gate (epoch or self, like chklocks/drainfwq); a backlog drains across
  epochs.
- Deadline stamping: createuwreq/drainfwq stamp creation_epoch + pending
  timeout; try_select_winner zeroes the deadline (the wall-clock lock
  window owns CONFIRMED rows -- chklocks always terminalizes them);
  terminal transitions stamp the retention window. Zero means "no
  deadline, never swept" (dclaim's zero-sentinel predicate).
- Payload compaction: non-winner UIC blobs clear at CONFIRMED; the
  inbound attestation copy and every stored UIC byte blob clear at
  terminal transitions -- retention keeps compact audit metadata only.
- Row-growth rails: per-leg UIC byte cap, candidate cap, source_tx_id /
  depositor / attestation-data caps at ingestion, and a projected
  pack_size whole-row guard before growing modifies -- all fail-closed
  (log + skip), never check(), per the never-throw dispatch contract.
- setconfig grows uwreq_pending_timeout_epochs + uwreq_retention_epochs
  (defaults 10/10; positive, ceiling-capped against deadline-stamp wrap).

Tests: 13 new cases. Unit: sweep auth/no-op/zero-budget + knob
validation/round-trip. Dispatch-suite integration driving real epoch
advances (the fixture now deploys sysio.system and satisfies the
emissions gate, per the emissions_tests recipe): pre-deadline advances
leave rows untouched; PENDING -> EXPIRED + compaction -> retention ->
erased; from-WIRE expiry refunds the full escrow fee-exempt; the
COMPLETED happy path ages out to steady state; 33 due rows against the
32-row budget prove the bound; oversized-UIC and candidate-cap rails
hold. Full contracts_unit_test suite green (520 cases).

Change-Id: Ifce803b6bfea1e7fa2668cd745d702b7845413c7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant