Repository navigation
Conversation
Moves sysio.swap's 10^decimals helper, unchanged, to sysio.opp.common/decimal.hpp so sysio.liq can size its desyndicate floor with the same definition. sysio.swap's wasm and abi are unchanged.
buildenv runs inline in sysio.epoch::advance. A READY attestation too large for any envelope made it abort, so one oversized row halted epoch advancement chain-wide, and its READY scan walked every outpost's rows each epoch with no bound, while sysio.liq::desyndicate lets any holder add rows. It now walks one outpost's queue through a (status, chain, lane) index and stops at the first row that does not fit, so a call reads about one envelope's worth of rows plus any it drops. The operator schedule (OPERATORS and BATCH_OPERATOR_GROUPS) packs ahead of other traffic, each lane oldest first: an outpost seats the next epoch's batch-operator group only from BATCH_OPERATOR_GROUPS, and in strict queue order a backlog queued before advance pushed it out of the envelope, so Solana refused the next epoch's deliverers. The schedule gets a fixed budget of 24 KiB, Solana's OPERATORS byte cap. A row larger than its lane can ever ship is dropped with a diagnostic: for the schedule, whatever an empty envelope cannot hold; for other traffic, whatever cannot fit beside a full schedule (7656 bytes). Without that bound a kept row could sit at the front of its lane forever and block every row behind it. An emptied queue still emits the epoch's envelope, since each outpost answers the depot's envelope with its own. The attestations table's unread bytype and byepoch indexes go. advance keeps the schedule within that budget. The OPERATORS roster listed every sysio.opreg row, and regoperator is permissionless, so unbonded registrations grew it without bound; it now lists ACTIVE and SLASHED operators only. Both outposts treat an omitted operator as not active, and SLASHED stays because Solana resolves a slash's target through its registry. A roster that would still take the schedule past the budget is withheld with a diagnostic, the way an empty batch window already is: only the queueout is skipped, advance continues, and the outposts keep their previous roster.
desyndicate accepted a single subunit, so each call bought a sysio-billed outbound attestation and a relayer-paid payout on the outpost. Each shadow token now carries a minimum, by default one hundredth of a whole token and at least one subunit, which the contract's own authority can change with setmindesyn.
huangminghuang
left a comment
There was a problem hiding this comment.
Fresh-deployment review findings; migration and backward-compatibility concerns intentionally excluded.
| if (ship_operators) { | ||
| queue_to_active_outposts(opp::types::ATTESTATION_TYPE_OPERATORS, encoded_operators); | ||
| } | ||
| if (have_next_group) { |
There was a problem hiding this comment.
ship_operators is false when the combined schedule exceeds the budget, but this branch still publishes BATCH_OPERATOR_GROUPS. The new window is therefore resolved against the previous roster. Ethereum stores unresolved names as address(0) seats that still count toward the quorum denominator, while Solana drops them; enough newly active or changed operators can make the next epoch impossible to deliver, and the correcting roster is then trapped inside an envelope no seated group can deliver. Publish the roster and dependent window atomically, or prove the window is resolvable against the last delivered roster.
| auto queue = atts.get_index<"byqueue"_n>(); | ||
| for (auto it = queue.lower_bound(first); it != queue.end() && it->by_queue() <= last;) { | ||
| const queue_lane lane = lane_of(it->type); | ||
| if (it->data.size() > max_attestation_data_bytes(lane)) { |
There was a problem hiding this comment.
queueout accepts any payload size, but this branch permanently erases non-schedule payloads above 7,656 bytes. createuwreq can reach this with current code: its size-cap rejection copies the oversized inbound SwapRequest.actor into SwapRevert.depositor, queues that refund, and buildenv then drops it, leaving source-outpost escrow without the refund. Ensure a never-throw producer emits a bounded refund or persists a recoverable failure before enqueueing an undeliverable row.
| { | ||
| const auto [first, last] = queue_span(AttestationStatus::ATTESTATION_STATUS_READY, chain_code); | ||
| auto queue = atts.get_index<"byqueue"_n>(); | ||
| for (auto it = queue.lower_bound(first); it != queue.end() && it->by_queue() <= last;) { |
There was a problem hiding this comment.
This loop has no scan or drop budget: oversized rows are erased without increasing estimated_bytes, so it can traverse an arbitrary run of them inside inline buildenv. If it exhausts transaction CPU, rollback restores every deletion and the next epoch retries the same run forever. Cap drops per invocation or move cleanup to a bounded, paginated action.
Bounds the depot's outbound attestation queue and the operator roster that feeds it, and puts a floor under
sysio.liq::desyndicate. These come from the CertiK Sysio Audit 1 preliminary report.Oversized attestations no longer halt epochs
buildenvruns inline insysio.epoch::advance. It aborted when a READY attestation was too large for any envelope, so one such row halted epoch advancement chain-wide.It now drops a row that is larger than its lane can ever ship, prints a
DROP attestationdiagnostic, and ships the rows behind it. If the drops empty the queue, the epoch's envelope still goes out, because each outpost answers the depot's envelope with its own.Bounded walk, operator schedule first
The READY scan walked every outpost's rows every epoch, and
desyndicatelet any holder grow the queue.A new
byqueueindex on(status, chain, lane)replacesbystatus.buildenvwalks one outpost's queue and stops at the first row that does not fit, so each call reads about one envelope's worth of rows plus any it drops. The unreadbytypeandbyepochindexes are removed.The operator schedule (OPERATORS and BATCH_OPERATOR_GROUPS) now packs ahead of other traffic, and each lane ships oldest first. Solana seats the next epoch's batch-operator group only from BATCH_OPERATOR_GROUPS, and
epoch_inadmits only that group. In strict queue order, a backlog queued beforeadvancecould push BATCH_OPERATOR_GROUPS out of the envelope and deadlock the bridge.The schedule gets a 24 KiB budget, equal to Solana's OPERATORS byte cap. Other rows are bounded by what fits beside a full schedule, 7,656 bytes, so every row the walk keeps eventually ships. Every current non-schedule payload is under about 1 KiB.
Roster: ACTIVE and SLASHED operators only, withheld over budget
The OPERATORS roster listed every
sysio.opregrow, andregoperatoris permissionless, so anyone could grow it. It now lists ACTIVE and SLASHED operators only.Desyndicate floor
desyndicateaccepted a single subunit, and each call bought a sysio-billed outbound attestation and a relayer-paid payout.Each shadow token now has a minimum: by default one hundredth of a whole token, and at least one subunit. The contract's own authority can change it with the new
setmindesynaction.precision_from_decimalsmoves from sysio.swap tosysio.opp.common/decimal.hpp, so both contracts share it.Notes
bystatus,bytypeandbyepochbecome a singlebyqueue(uint128).currency_stats.min_desyndicateandsetmindesyn.sysio.msgchandsysio.liq(wasm and abi), andsysio.epoch(wasm only).MAX_OPERATORS. At nominal operator counts, about two dozen slashes would freeze Solana's roster. If Solana could settle a SLASH byop_address, the depot could drop settled SLASHED rows.max_available_*enforcement remain follow-ups.SysioContractTypes.tsneeds regenerating for the liq changes.