Repository navigation
[WNS-56] OPP: bound the operator roster to the schedule window and cap queueout - #692
Conversation
| // A row no envelope can carry would abort every later `buildenv`, and with it `sysio.epoch::advance`. | ||
| // Rejecting it here reverts only the producer's transaction, including the state change it queued | ||
| // the row for. | ||
| check(data.size() <= MAX_ATTESTATION_DATA_BYTES, |
There was a problem hiding this comment.
High — bound READY queue cardinality, not only each payload. This check prevents one oversized attestation, but buildenv still walks every READY row across all chains and copies every matching row before applying the envelope-byte budget. A holder-authorized sysio.synd::desyndicate creates one system-paid READY row per call, while min_desyndicate constrains value rather than row count. Enough individually valid rows can therefore starve later OPERATORS / BATCH_OPERATOR_GROUPS updates beyond the outpost schedule window, or exhaust finishadv resources and roll the transaction back with the backlog intact. Please add a chain-scoped ordered index plus a fixed scan/row budget, prioritize epoch-control attestations, and include a bounded recovery/prune path for existing backlog. This is the WIRE-415 path already disclosed in the PR body, but it remains material if this PR closes WNS-56/WIRE-406.
There was a problem hiding this comment.
Agreed. This PR stays scoped to WNS-56; the queue fix is WIRE-415, in #696, stacked on this one:
- a chain-scoped READY index that puts OPERATORS and BATCH_OPERATOR_GROUPS ahead of everything else, so a backlog cannot hold back the schedule;
- a
buildenvwalk with a fixed byte and row budget, so its cost no longer grows with the backlog; queueoutrefusing non-schedule rows once a chain's READY backlog reaches a cap.desyndicatethen reverts as a whole, burn included, and the holder retries later.
No prune path: a READY DESYNDICATE_LIQ is a payout whose LIQ is already burned, so dropping it would lose the holder's funds. The bounded walk drains a backlog one budget at a time.
|
Low — update documentation made stale by the window-only roster change. |
|
Fixed in 2dcac2f. All four now describe the window's roster and the 8 × 16 = 128 limits, and the |
|
|
||
| entry.addresses.push_back(std::move(chain_addr)); | ||
| ++link_it; | ||
| ops_attest.operators.push_back(std::move(entry)); |
There was a problem hiding this comment.
High — cap or chunk the roster to what Ethereum can dispatch atomically. The new limits permit 8 groups × 16 members, while finishadv encodes all 128 members into one OPERATORS attestation. Against current wire-ethereum/next (0b5184ce), I reproduced the maximum roster under the 16,777,216 transaction gas target: dispatch reverts on the first entry with OPP_HandlerGasExhausted (handler gas bound ≈13.94M). Ethereum resumes only between attestations, so when this atomic handler fails first, no cursor progress is committed and retries at the supported limit cannot advance the epoch. Please either lower the accepted roster ceiling, chunk OPERATORS, or make the Ethereum handler internally resumable, and add an 8×16 cross-repo gas-cap test.
There was a problem hiding this comment.
Lowered, as you suggest: setconfig now takes at most 7 per group, Ethereum's OPERATORS_PER_GROUP, so the window and the roster are at most 8 × 7 = 56 (06ceae3). On wire-ethereum/next (0b5184ce), with every entry carrying an EVM key and an SVM address and every operator outside the delivering group unseen, it fits in one transaction:
| Roster | Peak transaction | Result |
|---|---|---|
| 8 × 7, 49 unseen | 9,312,094 gas (55.5% of 16,777,216) | dispatches in one transaction |
| the same, outbound queue full | 12,422,290 (74.0%) | one transaction |
| 63 entries, 56 unseen, queue full | 13,365,585 (79.7%) | one transaction |
| 8 × 16, 121 unseen | — | OPP_HandlerGasExhausted, as you found |
The 8 × 7 case will be a permanent test in wire-ethereum's OPPWorstCase suite, in its own PR.
|
Measured this branch (2dcac2f) against the Solana outpost's heap, from the wire-solana side (SOL-392 / CertiK WNS-185). Short version: with this PR's roster bound, every depot envelope shape we measured fits the outpost's 256,000 B dispatch heap frame. Without it (master), a self-registered roster of a few hundred operators stalls Solana inbound permanently. Method: the real What this branch bounds, as we read it: OPERATORS comes only from the schedule window, at most 8 × 16 = 128 operators; authex allows at most one link per chain kind (SVM 32 B, EVM 33 B); names are at most 12 characters. So the largest entry is 100 B and the largest roster 12,800 B, well under the 32,232 B Drain-path results (budget 256,000 B), each with a full envelope of desyndications:
Not measured: windows mixing slashed and SVM-less members, a desyndication that actually pays out (Token-2022 CPI), and byte-exact depot encoding (we used the Rust models encoder; sizes match zpp for these shapes). One coupling to keep: the outpost's |
| static constexpr uint32_t MAX_SCHEDULED_BATCH_OPERATORS = 1000; | ||
| /// Upper bound on the number of batch-operator groups: Solana's operator registry seats at most | ||
| /// `MAX_OPERATOR_GROUPS` (8), and a window an outpost cannot seat stalls that outpost. | ||
| static constexpr uint32_t MAX_BATCH_OP_GROUPS = 8; |
There was a problem hiding this comment.
Medium — keep cluster-tool schedule validation in sync with these limits. wire-tools-ts still validates with MAX_BATCH_OP_GROUPS = 255, MAX_OPERATORS_PER_EPOCH = 100, and MAX_SCHEDULED_BATCH_OPERATORS = 1000 in BatchOperatorSchedule.ts. Configurations such as 9 groups of 1 or one group of 17 therefore pass tool preflight and fail later in epoch::setconfig during cluster startup. Please update those constants and their boundary tests in a companion change, ideally from a shared or generated source of truth, so invalid schedules are rejected before startup.
huangminghuang
left a comment
There was a problem hiding this comment.
Requesting changes for the blocking issues identified inline: the accepted 8×16 roster cannot currently be dispatched atomically by a fresh Ethereum outpost within the supported gas limit, and the cluster-tool preflight still accepts schedule configurations that the updated contract rejects. Please resolve or explicitly constrain these cross-repository boundaries before merging.
|
@valthon Thanks, this is the measurement the Solana side needed. One change since: HM found Ethereum cannot apply a 128-entry roster in one handler call ( A smaller roster leaves room for more desyndications (about 350 instead of 259), so I ran your harness from Wire-Network/wire-solana#591 at 8 × 7 (
|
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed current head 06ceae3 and its merge with the current base. The previous blocking findings are addressed: the 8 × 7 ceiling fits Ethereum’s atomic handler budget, and Wire-Network/wire-tools-ts#154 mirrors the contract validation. With the confirmed pre-launch/fresh-state rollout, no legacy-state migration is required. Current-head CI and the focused contract suites pass. Approved.\n\nWire-Network/wire-sysio#696 remains the disclosed stacked fix for the pre-existing READY-queue cardinality path and must land before deployment; #154 should likewise be coordinated before deployment.
#692 caps groups at 7, so the lane test now mirrors 7 and builds the largest window at 8 x 7: 6,806 bytes, down from 15,446 at 8 x 16. `SCHEDULE_LANE_BYTES` stays 24 KiB, which still holds a window at Solana's 16 per group.
|
Low — correct the stale slash-propagation comment.
Please qualify the comment: a slashed member that remains seated is published as |
The base branch was changed.
| (void)out(ops_attest); | ||
| // An empty roster tells an outpost nothing (Solana skips it as an anomaly), so an empty window | ||
| // queues none; the BatchOperatorGroups withhold below reports that state. | ||
| if (!ops_attest.operators.empty()) { |
There was a problem hiding this comment.
High — an empty post-slide window preserves removed operators’ outpost authority.
This new guard suppresses the authoritative OPERATORS snapshot when the window becomes empty. On the same transition, finishadv also withholds BATCH_OPERATOR_GROUPS when the next group is empty (lines 891–939), so neither outpost receives a revocation.
This is reachable with the allowed 1-group/1-operator configuration. The sole non-bootstrap operator can deliver the current epoch and then request its full collateral withdrawal; opreg::withdraw immediately re-evaluates eligibility. The next slide removes that now-non-ACTIVE member and selects an empty tail. Previously, the full roster would have carried UNKNOWN/SLASHED/TERMINATED; this branch emits neither control attestation.
An initialized Solana outpost retains the old ACTIVE mapping and active group. Ethereum only clears an omitted/inactive resident seat when its OPERATORS handler runs, and its retained one-slot anchor can authorize that old address for the next epoch. With a 1×1 quorum, the removed key can submit a forged next envelope containing OPERATORS and BATCH_OPERATOR_GROUPS entries that re-seat itself.
Please provide an explicit fail-closed revocation shape that both outposts apply (for example bounded terminal tombstones or coordinated authoritative-empty support), or stop advancement when no authoritative next group can be conveyed. Add an initialized-outpost regression covering a nonempty→empty transition and assert that the old key cannot submit the next epoch. The new opreg slash comment is also still inaccurate on this path because the claimed group update is withheld.
There was a problem hiding this comment.
I built the tombstone variant to test this: every roster listed the expired group's departing members with their real status. I also wrote initialized-outpost regressions for it on Ethereum and Solana. It doesn't close the attack, so I've left it out and squashed the PR.
- Only the departing operator can deliver envelope N. Any revocation for N, whether a tombstone or an authoritative-empty, travels in envelope N. Both outposts admit only the group that envelope N−1 seated for N, which is the departing operator. Nothing else delivers it either:
batch_operator_pluginrelays only for the depot's front group (outpost_opp_job.cpp:54), and that group is empty after this slide. - A hostile operator can forge N whatever the depot emits. With a 1×1 quorum, its forged N re-seated it in both reproductions: a scratch run on Ethereum
nextand the Solana harness. - This predates [WNS-56] OPP: bound the operator roster to the schedule window and cap queueout #692. Master's full roster travelled in the same envelope.
- Stopping advancement doesn't change what the outposts accept for N. With an empty window no one can
deliverto the depot either, so the bridge halts at N anyway.
What happens to the old key after N:
- Ethereum: the single-group window maps exactly one epoch ahead. With BATCH_OPERATOR_GROUPS withheld, N+1 is unmapped and the key reverts with
OPP_UnmappedEpochGroup, tombstone or not. - Solana: the active group is not tied to an epoch, so the seated group stays authorized until a groups update arrives.
Closing this needs changes outside the roster, tracked in WIRE-423:
- Tie Solana's active group to the epoch it was shipped for, as Ethereum does.
- Keep a scheduled operator from withdrawing below its minimum until its scheduled epochs close.
- Elect a relay when the depot's window is empty.
The slash comment no longer claims the groups update. It now says rosters list the operator as SLASHED while it is seated and leave it out once its group has left.
The OPERATORS roster listed every sysio.opreg row, and regoperator is self-authorized, so anyone with accounts could grow it until it no longer fit an envelope. buildenv then aborted inside sysio.epoch::advance, and every later advance failed the same way (CertiK WNS-56, WIRE-406). Well before that, a roster with enough operators Ethereum had never seen ran its OPERATORS handler out of gas (WIRE-342). Outposts read the roster only to map the window's members to the keys they deliver with, so finishadv now lists just the operators the window seats, each with its current status. setconfig caps the window at the outposts' limits, groups of at most 7 (Ethereum's OPERATORS_PER_GROUP) and at most 8 of them (Solana's MAX_OPERATOR_GROUPS), so the roster holds at most 56 entries. On wire-ethereum next, a 56-entry roster with 49 operators Ethereum has never seen dispatches in one transaction: 9.3M gas, 12.4M with the outbound queue full, of EIP-7825's 16,777,216. queueout rejects data larger than an otherwise empty envelope can carry, so the producer's transaction fails instead of a later buildenv. buildenv's single-attestation checks are now unreachable invariants, and the envelope size constants moved into the msgch header. The docs describe the window's roster and the 7 x 8 ceilings. At most 7 operators over 10 epochs is 70 payout credits, so the 10-epoch history bound is the cadence limit and the 100-credit cap stays only as a backstop.
139ec03 to
0e82d29
Compare
| | Category | Types | Direction | Purpose | | ||
| |----------|-------|-----------|---------| | ||
| | **Operator Rosters** | `OPERATORS`, `BATCH_OPERATOR_GROUPS` | depot → outpost | Full operator roster and the next epoch's batch-operator group, sent every epoch | | ||
| | **Operator Rosters** | `OPERATORS`, `BATCH_OPERATOR_GROUPS` | depot → outpost | The operators the schedule window seats, with their chain addresses, and the next epoch's batch-operator group, sent every epoch | |
There was a problem hiding this comment.
Low — qualify the empty-schedule exception. This says OPERATORS and BATCH_OPERATOR_GROUPS are “sent every epoch,” but finishadv now deliberately queues no OPERATORS when the post-slide window is empty and withholds BATCH_OPERATOR_GROUPS when the next group is empty. Please describe these as the normal non-empty-schedule updates, or mention the empty-window exception, so the overview does not promise control attestations that will not exist.
The overview said OPERATORS and BATCH_OPERATOR_GROUPS are sent every epoch, but an empty window sends no roster and an empty next group withholds BATCH_OPERATOR_GROUPS (HM's review).
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed the complete diff at fa823e2 against current master, including the documentation follow-up. I found no remaining actionable issues.
The previously raised empty-window authority concern is the pre-existing WIRE-423 recovery gap rather than a regression in this change. The follow-up changes only OVERVIEW.md, so the prior source, focused-test, ABI, and byte-for-byte WASM validation remains applicable. Exact-head GitHub CI currently has no failures; macOS is running and the Linux matrix is queued.
wire-tools-ts#154 is the compatible 7 x 8 configuration companion and must merge or co-deploy. The separate WIRE-415 queue-cardinality fix in #696 remains a deployment dependency and needs restacking/revalidation after this branch rewrite.
Brings in #658 (WNS-57 and WNS-51 emission headroom). The text files merged without a hunk both sides edited. sysio.epoch.wasm is rebuilt from the merged sources, and every other contract matches its committed wasm and ABI.
|
Low — documentation ( |
Brings in #651, #659 and #701. sysio.epoch.wasm is rebuilt from the merged sources, and every other contract matches its committed wasm and ABI. MAX_OPERATORS_PER_EPOCH keeps the cap of 7 with #659's cost wording, one recorddel and termcheck per member. #659's shared rotation walk now checks the window roster: a member terminated as its group expires is no longer listed.
The budget is t5_distributable - t5_floor, so "refuses a budget below t5_floor + total_distributed + pending_emission_amount" counted the floor twice; setemitcfg refuses a t5_distributable below that sum (HM's review).
huangminghuang
left a comment
There was a problem hiding this comment.
Targeted re-review completed through 8594a73. No blocking findings; the remaining P3 documentation clarification for the zero-active-outpost case is non-blocking.
Brings in #692 (merged as a62f42c, superseding this branch's copies of its commits), #592, #659, #658, #651 and the rest of master. - `buildenv` keeps #592's retention: shipped rows are erased once packed, the chain links come from the newest `byoutepoch` row, and the acknowledged-prefix sweep runs before the inbound PROCESSED sweep. - `shipped_attestations` reads the newest `outenvelopes` row, since unacknowledged emits now stay, and the new readers decode with `fixture_abi_yield_function()` after #684's sweep. - Two locals in `buildenv` are renamed so they no longer shadow (`by_queue`, `inbound`). - `sysio.msgch.wasm` rebuilt.
Summary
Fixes WIRE-406 (CertiK WNS-56) and WIRE-342. The OPERATORS roster listed every
sysio.opregrow, andregoperatoris self-authorized, so anyone with accounts could grow it until it no longer fit an envelope.buildenvthen aborted insidesysio.epoch::advance, and every later advance failed the same way. Well before that, a roster with enough operators Ethereum had never seen ran its OPERATORS handler out of gas, so Ethereum could not apply it at all (WIRE-342).finishadvbuilds the roster from the window: every operator its groups seat, with its current status and authex links. Outposts read the roster only to map those members to the keys they deliver with (Solana'sepoch_ingate and quorum guard, Ethereum's window resolution), so nothing they use is lost. A seated member keeps its real status, so a slashed member still makes Solana refuse the window. An empty window queues no roster.sysio.epoch::setconfigcaps the window at the outposts' limits: groups of at most 7 (Ethereum'sOPERATORS_PER_GROUP) and at most 8 of them (Solana'sMAX_OPERATOR_GROUPS). That bounds the roster at 56 entries, about 5.6 KB. Ethereum applies the roster in one handler call: onwire-ethereum/nexta 56-entry roster with 49 operators it has never seen dispatches in one transaction, 9.3M gas (12.4M with the outbound queue full) of EIP-7825's 16,777,216, where 8 × 16 runs out of handler gas. The separate window ceiling could no longer fail and is removed.queueoutrejectsdataoverMAX_ATTESTATION_DATA_BYTES(32,232), the most an otherwise empty envelope carries. The producer's transaction fails, along with the state change it queued the row for, instead of a laterbuildenv.buildenv's two single-attestation checks are now unreachable and documented as invariants. The envelope size constants moved into the msgch header.SysioContractTypesregeneration, and no Ethereum or Solana change. Onlysysio.epoch.wasmandsysio.msgch.wasmare rebuilt.Notes
queueout, was never reachable:queueoutaccepts only the epoch, opreg, synd and msgch authorities.sysio.syndmin_desyndicate. WIRE-415, which bounds the READY queue and puts the schedule attestations first, is [WIRE-415] OPP: bound the outbound attestation queue #696, stacked on this one.BatchOperatorSchedule; its PR links here and should merge with this one.Tests
advance_roster_lists_only_the_window: 16 unbonded registrations, an unseated ACTIVE batch operator and an ACTIVE challenger stay out of the roster.advance_roster_keeps_a_seated_slashed_member_until_it_leaves: a slashed member stays listed as SLASHED while seated and drops out once its group slides off.advance_queues_no_roster_for_an_empty_window: with no schedule, no OPERATORS attestation and no envelope.queueout_rejects_an_attestation_no_envelope_can_carryandbuildenv_ships_an_attestation_at_the_cap_alone: one byte over the cap is refused; a row at the cap ships alone and the next row waits.setconfigceiling tests (7 per group accepted and 8 rejected, and the largest window, 8 groups of 7), the emissions boundaries (cadence 10 accepted and 11 rejected at 7 per group, an 8-operator group rejected, and the legacy-cadence recovery at 7 per group), and [WNS-34] Fix WIRE-385 post-mutation operator roster publication #603's three roster assertions, where a slashed or terminated operator that has left the window is now absent.contracts_unit_testpasses 952/952 on current master.