From aa547890a049b77cf27a70257d374d86640889e1 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 17 Aug 2026 14:53:30 -0500 Subject: [PATCH 1/3] WIRE-342: send the OPERATORS roster only when it changes `sysio.epoch::advance` re-encoded the entire `sysio.opreg::operators` table -- every status, including never-bonded UNKNOWN and never-erased TERMINATED rows -- and fanned it to every active outpost on EVERY epoch, unconditionally. The roster changes on registration/activation/slash; advance runs every epoch. So the depot re-shipped ~10 KB of byte-identical data per outpost per epoch, consuming roughly a third of every 32 KiB envelope, to say nothing. Re-sending bought nothing. An outpost cannot miss an envelope and continue -- both enforce strict epoch sequencing (ETH OPPInbound.sol:429, SOL epoch_in.rs:63-66), so a missed envelope stalls rather than diverges. Ethereum's handler has no skip path at all. Solana's three skips are pure functions of the payload, so a byte-identical re-send reproduces them exactly. No outpost re-converges via the rebroadcast. Six changes: 1. Send on change. A per-outpost digest of the encoded payload (`epoch::rosterdig`) gates `queueout`. The full roster still ships when it changes, so ETH's generation-stamp model and SOL's destructive replace are untouched. Absence of a row is what makes a newly activated outpost receive it unconditionally. No periodic re-send: every case where an outpost could want a roster it was already sent is an operational act needing depot coordination anyway (re-init resets the epoch cursor alongside the registry). 2. Filter to ACTIVE + SLASHED via the existing `bystatus` index. SLASHED must stay: `opreg::slash` flips the row BEFORE emitting OPERATOR_ACTION(SLASH), and SOL resolves a slash's target through this roster, so ACTIVE-only would silently no-op every slash. Excluding UNKNOWN is what makes a never-bonded registration cost zero envelope bytes. 3. Enforce `op_config.max_available_*` in `regoperator`. Declared, validated and persisted since the registry landed -- and read by nothing. Bootstrapped operators bypass it. 4. Cross-validate those ceilings against the envelope budget in `opreg::setconfig`, so a governance change cannot produce a roster no envelope can carry. `sysio.epoch` already does this for the sibling BATCH_OPERATOR_GROUPS attestation. 5. `buildenv`'s single-attestation guard now reports att_id, bytes and budget. It aborts `advance` inline, halting epoch advancement chain-wide, so it should not be anonymous. 6. Bound the per-operator authex address walk. The envelope budget moves to `sysio.opp.common/opp_envelope_budget.hpp` so epoch derives its ceiling from it rather than restating a consensus-visible cap. msgch re-exposes the values as class members (aliases, not copies) and its call sites are unchanged. Artifacts: epoch/opreg/msgch only -- the three with source changes. epoch.wasm carries +112 bytes from the CDT kv-header change (PR #107) that also moves several untouched contracts; those belong to a separate toolchain-refresh commit. opreg +3691 and msgch +2033 are entirely this change; verified against a same-CDT build of master. Full contracts_unit_test --sys-vm: No errors detected. Change-Id: I9d1ea243859297860de0336061ebab60c727e3a0 --- .../include/sysio.epoch/sysio.epoch.hpp | 102 +++++++++++ contracts/sysio.epoch/src/sysio.epoch.cpp | 136 +++++++++++---- contracts/sysio.epoch/sysio.epoch.abi | 36 ++++ contracts/sysio.epoch/sysio.epoch.wasm | Bin 75691 -> 76106 bytes .../include/sysio.msgch/sysio.msgch.hpp | 14 ++ contracts/sysio.msgch/src/sysio.msgch.cpp | 55 +++--- contracts/sysio.msgch/sysio.msgch.wasm | Bin 151948 -> 153981 bytes .../sysio.opp.common/opp_envelope_budget.hpp | 52 ++++++ contracts/sysio.opreg/src/sysio.opreg.cpp | 82 +++++++++ contracts/sysio.opreg/sysio.opreg.wasm | Bin 88451 -> 92142 bytes contracts/tests/sysio.dispatch_tests.cpp | 165 +++++++++++++++++- contracts/tests/sysio.opreg_tests.cpp | 94 ++++++++++ 12 files changed, 666 insertions(+), 70 deletions(-) create mode 100644 contracts/sysio.opp.common/include/sysio.opp.common/opp_envelope_budget.hpp diff --git a/contracts/sysio.epoch/include/sysio.epoch/sysio.epoch.hpp b/contracts/sysio.epoch/include/sysio.epoch/sysio.epoch.hpp index 5a8e6a5154..b151f55e27 100644 --- a/contracts/sysio.epoch/include/sysio.epoch/sysio.epoch.hpp +++ b/contracts/sysio.epoch/include/sysio.epoch/sysio.epoch.hpp @@ -8,6 +8,11 @@ #include #include #include +// For the OPP envelope budget — the roster ceiling below derives from it rather than +// restating a consensus-visible cap in a second place. Deliberately the shared +// opp.common header and NOT `sysio.msgch.hpp`: several contracts include this header +// without msgch on their include path. +#include namespace sysio { @@ -118,6 +123,45 @@ namespace sysio { using blocklog_t = sysio::kv::table<"blocklog"_n, blocklog_key, blocklog_entry>; + /// Digest of the last OPERATORS attestation queued for one outpost. + /// + /// `advance` re-derives the roster every epoch but queues it only when this + /// digest changes, so a static roster costs zero envelope bytes. The roster + /// changes on registration / activation / slash — rarely — while `advance` + /// runs every epoch, and re-sending it bought nothing: an outpost cannot miss + /// an envelope and continue (both outposts enforce strict epoch sequencing, so + /// a missed envelope stalls rather than diverges), and an outpost that DROPS a + /// roster does so on conditions that are pure functions of the payload, which a + /// byte-identical re-send reproduces exactly. + /// + /// Per-outpost rather than global because a newly-activated outpost has never + /// received the roster: the ABSENCE of a row is what makes its first `advance` + /// send unconditionally. Rows are reused, never erased, mirroring + /// `msgch::outpost_consensus_entry`. + /// + /// There is deliberately no periodic re-send. Every case in which an outpost + /// could want a roster it was already sent is an explicit operational act that + /// needs depot-side coordination regardless: an outpost re-init resets its epoch + /// cursor alongside its registry, so it rejects every envelope until it is + /// re-bootstrapped, and raising an outpost's own roster ceiling is a program + /// upgrade with a runbook. A timer would serve those late while costing a send + /// forever. + struct roster_digest_key { + uint64_t chain_code; + uint64_t primary_key() const { return chain_code; } + SYSLIB_SERIALIZE(roster_digest_key, (chain_code)) + }; + + struct [[sysio::table("rosterdig")]] roster_digest_entry { + uint64_t chain_code = 0; + checksum256 digest = {}; + uint32_t sent_at_epoch = 0; + + SYSLIB_SERIALIZE(roster_digest_entry, (chain_code)(digest)(sent_at_epoch)) + }; + + using rosterdig_t = sysio::kv::table<"rosterdig"_n, roster_digest_key, roster_digest_entry>; + // Well-known accounts static constexpr name CHALG_ACCOUNT = "sysio.chalg"_n; static constexpr name MSGCH_ACCOUNT = "sysio.msgch"_n; @@ -172,6 +216,64 @@ namespace sysio { /// envelope, and `buildenv` would carry the remainder to the next epoch. static constexpr uint32_t MAX_SCHEDULED_BATCH_OPERATORS = 1000; + /// Upper bound on the authex-linked chain addresses carried for ONE operator + /// in the OPERATORS attestation. + /// + /// The per-operator address walk is otherwise unbounded — `authex::links` has + /// no cap on how many keys one account may link — so a single account could + /// inflate one roster entry without limit and push the attestation past the + /// envelope on its own. An operator legitimately needs one address per + /// registered outpost chain (two today: EVM + SVM), so 8 is generous headroom + /// while still bounding the entry. It also matches what the Solana outpost + /// already assumes when sizing its own pre-decode payload gate. + static constexpr uint32_t MAX_OPERATOR_CHAIN_ADDRESSES = 8; + + // ----------------------------------------------------------------------- + // OPERATORS roster ceiling + // ----------------------------------------------------------------------- + // + // `MAX_SCHEDULED_BATCH_OPERATORS` above does this arithmetic for the sibling + // BATCH_OPERATOR_GROUPS attestation; it was never done for OPERATORS, which is + // ~5x fatter per member and drawn from an UNBOUNDED registry rather than a + // bounded schedule window. `sysio.opreg::setconfig` validates its summed + // `max_available_*` ceilings against the result, so a governance change cannot + // raise the registry past what an envelope can carry. + + /// Chain addresses carried per operator at the REALISTIC shape: one per + /// registered outpost. The launch outpost set is {ETH, SOL}, and every operator + /// type must bond on every registered outpost, so two is the shape to size + /// against — not `MAX_OPERATOR_CHAIN_ADDRESSES`, which is the defensive per-entry + /// cap for a pathological account, not the expected census. + static constexpr uint32_t ROSTER_ADDRESSES_PER_OPERATOR = 2; + + /// Encoded bytes of one `ChainAddress`: 1 B tag + 1 B length + 2 B `kind` varint + /// + 2 B inner tag/length + a 33-byte compressed secp256k1 key (Ed25519 is 32, + /// so 33 is the worst case). + static constexpr uint32_t ROSTER_BYTES_PER_ADDRESS = 39; + + /// Encoded bytes of one `OperatorEntry`: a maximum-length WIRE account name + /// (4 B framing + 13 B) + its addresses + 2 B `type` + 3 B `status` (SLASHED is + /// 241, a two-byte varint) + 2 B for the repeated-field framing in `Operators`. + static constexpr uint32_t ROSTER_BYTES_PER_OPERATOR = + 17 + ROSTER_ADDRESSES_PER_OPERATOR * ROSTER_BYTES_PER_ADDRESS + 2 + 3 + 2; + + /// The share of one envelope the roster may claim. + /// + /// Sizing the ceiling against the WHOLE budget would let a legal configuration + /// produce a roster that fills an envelope by itself — the exact hazard + /// `MAX_SCHEDULED_BATCH_OPERATORS` names ("little room for value-bearing + /// attestations in the same envelope"). Half leaves the remainder for + /// BATCH_OPERATOR_GROUPS and the SWAP_REMIT / WITHDRAW_REMIT / RESERVE_READY + /// traffic that settles user value, which must not be deferred behind the roster. + static constexpr uint32_t ROSTER_ENVELOPE_SHARE_DIVISOR = 2; + + /// Ceiling on the total registered (non-bootstrapped) operator count, derived + /// from the envelope budget that `sysio.msgch` owns rather than restating it. + static constexpr uint32_t MAX_ROSTER_OPERATORS = + static_cast(sysio::opp::SINGLE_ATTESTATION_BUDGET_BYTES + / ROSTER_ENVELOPE_SHARE_DIVISOR + / ROSTER_BYTES_PER_OPERATOR); + private: // Namespace alias for OPP protobuf enum types diff --git a/contracts/sysio.epoch/src/sysio.epoch.cpp b/contracts/sysio.epoch/src/sysio.epoch.cpp index 5dc4d4e9f1..0134a8cc07 100644 --- a/contracts/sysio.epoch/src/sysio.epoch.cpp +++ b/contracts/sysio.epoch/src/sysio.epoch.cpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace sysio { @@ -57,6 +58,53 @@ inline bool is_active_outpost(const sysio::chains::chain_row& row) { return row.active && !row.is_depot; } +/// Build one OPERATORS-roster entry for `op`, collecting its authex-linked chain +/// addresses from the `byname` index. +/// +/// The address walk is bounded at `epoch::MAX_OPERATOR_CHAIN_ADDRESSES`: `authex::links` +/// places no cap on how many keys one account may link, so an unbounded walk lets a +/// single account inflate one roster entry without limit — and the roster is the one +/// attestation that grows with adoption, so an oversized entry can push it past the +/// outbound envelope on its own. +template +opp::attestations::OperatorEntry build_operator_entry(const opreg::operator_entry& op, + LinksByNameIndex& links_by_name) { + opp::attestations::OperatorEntry entry; + entry.account.name = op.account.to_string(); + entry.type = op.type; + entry.status = op.status; + + // Store raw public key bytes from the variant (33 bytes for EM/secp256k1, + // 32 bytes for ED/Ed25519). + auto link_it = links_by_name.lower_bound(op.account.value); + while (link_it != links_by_name.end() && link_it->username == op.account && + entry.addresses.size() < epoch::MAX_OPERATOR_CHAIN_ADDRESSES) { + opp::types::ChainAddress chain_addr; + chain_addr.kind = static_cast(link_it->chain_kind); + + std::visit([&](const auto& key_data) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + // EM (secp256k1 compressed) — 33 bytes in key.key + chain_addr.address.assign(key_data.key.begin(), key_data.key.end()); + } else if constexpr (std::is_same_v) { + // ED (Ed25519) — 32 bytes + chain_addr.address.assign( + reinterpret_cast(key_data.data()), + reinterpret_cast(key_data.data() + key_data.size())); + } else if constexpr (std::is_same_v) { + // K1/R1 (secp256k1/P-256 compressed) — 33 bytes + chain_addr.address.assign(key_data.begin(), key_data.end()); + } + // Skip BLS keys — not used for chain address linking + }, link_it->pub_key); + + entry.addresses.push_back(std::move(chain_addr)); + ++link_it; + } + return entry; +} + struct emissions_gate_result { bool ready = false; bool is_pay_epoch = false; // true on the period-boundary epoch where payepoch fires @@ -682,61 +730,62 @@ void epoch::advance() { std::make_tuple(state.current_epoch_index) ).send(); - // Queue OPERATORS attestation (full roster with authex chain addresses) for each outpost. + // Queue the OPERATORS attestation for each outpost — but ONLY when the roster's + // content changed since that outpost's last send (see `epoch::roster_digest_entry`). + // // IMPORTANT: Must come before BATCH_OPERATOR_GROUPS so that the ETH outpost's // _handleOperators populates operatorEthAddress before _handleBatchOperatorGroups - // looks up those addresses. + // looks up those addresses. Skipping an UNCHANGED roster cannot break that ordering: + // both outposts retain the roster they already hold (ETH never prunes its address + // map; SOL only ever replaces it wholesale), and any newly-scheduled operator is by + // definition an ACTIVE batch operator, so its arrival IS a roster change and sends in + // the very epoch it is first scheduled. { opp::attestations::Operators ops_attest; opreg::operators_t opreg_ops(OPREG_ACCOUNT); authex::links_t authex_links(AUTHEX_ACCOUNT); auto links_by_name = authex_links.get_index<"byname"_n>(); + auto status_idx = opreg_ops.get_index<"bystatus"_n>(); - for (auto it = opreg_ops.begin(); it != opreg_ops.end(); ++it) { - opp::attestations::OperatorEntry entry; - entry.account.name = it->account.to_string(); - entry.type = it->type; - entry.status = it->status; - - // Collect all authex-linked chain addresses for this operator. - // Store raw public key bytes from the variant (33 bytes for EM/secp256k1, - // 32 bytes for ED/Ed25519). - auto link_it = links_by_name.lower_bound(it->account.value); - while (link_it != links_by_name.end() && link_it->username == it->account) { - opp::types::ChainAddress chain_addr; - chain_addr.kind = static_cast(link_it->chain_kind); - - std::visit([&](const auto& key_data) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - // EM (secp256k1 compressed) — 33 bytes in key.key - chain_addr.address.assign(key_data.key.begin(), key_data.key.end()); - } else if constexpr (std::is_same_v) { - // ED (Ed25519) — 32 bytes - chain_addr.address.assign( - reinterpret_cast(key_data.data()), - reinterpret_cast(key_data.data() + key_data.size())); - } else if constexpr (std::is_same_v) { - // K1/R1 (secp256k1/P-256 compressed) — 33 bytes - chain_addr.address.assign(key_data.begin(), key_data.end()); - } - // Skip BLS keys — not used for chain address linking - }, link_it->pub_key); - - entry.addresses.push_back(std::move(chain_addr)); - ++link_it; + // ACTIVE and SLASHED only — never the full table. + // + // UNKNOWN (registered but never bonded) has no consumer on either outpost: it + // cannot be scheduled and cannot commit. Excluding it is what makes a never-bonded + // registration cost zero envelope bytes, so growing the broadcast requires bonding + // collateral per operator rather than being free. TERMINATED rows are settled + // history and are never erased, so including them would make the roster grow + // monotonically over the network's lifetime even under perfect operator churn. + // + // SLASHED MUST stay in: `opreg::slash` flips the row to SLASHED *before* it emits + // OPERATOR_ACTION(SLASH), and the Solana outpost resolves a slash's target through + // this roster — so an ACTIVE-only roster would silently no-op every slash. + for (const auto status : { OperatorStatus::OPERATOR_STATUS_ACTIVE, + OperatorStatus::OPERATOR_STATUS_SLASHED }) { + for (auto it = status_idx.lower_bound(magic_enum::enum_integer(status)); + it != status_idx.end() && it->status == status; ++it) { + ops_attest.operators.push_back(build_operator_entry(*it, links_by_name)); } - - ops_attest.operators.push_back(std::move(entry)); } std::vector encoded; auto out = zpp::bits::out{encoded, zpp::bits::no_size{}}; (void)out(ops_attest); + // Content digest over the exact bytes each outpost would receive. Every outpost is + // sent the identical payload, so one digest compares against every per-outpost row. + const checksum256 roster_digest = sysio::keccak(encoded.data(), encoded.size()); + + epoch::rosterdig_t rosterdigs(get_self()); sysio::chains::chains_t chains_tbl(CHAINS_ACCOUNT); for (auto it = chains_tbl.begin(); it != chains_tbl.end(); ++it) { if (!is_active_outpost(*it)) continue; + + const auto digest_pk = epoch::roster_digest_key{it->code.value}; + const bool seen = rosterdigs.contains(digest_pk); + if (seen && rosterdigs.get(digest_pk).digest == roster_digest) { + continue; // unchanged since this outpost's last send — nothing to say + } + action( permission_level{get_self(), "owner"_n}, MSGCH_ACCOUNT, @@ -747,6 +796,19 @@ void epoch::advance() { encoded ) ).send(); + + if (seen) { + rosterdigs.modify(ram_payer, digest_pk, [&](auto& row) { + row.digest = roster_digest; + row.sent_at_epoch = state.current_epoch_index; + }); + } else { + rosterdigs.emplace(ram_payer, digest_pk, epoch::roster_digest_entry{ + .chain_code = it->code.value, + .digest = roster_digest, + .sent_at_epoch = state.current_epoch_index, + }); + } } } diff --git a/contracts/sysio.epoch/sysio.epoch.abi b/contracts/sysio.epoch/sysio.epoch.abi index 7d7ed6b341..5c90973995 100644 --- a/contracts/sysio.epoch/sysio.epoch.abi +++ b/contracts/sysio.epoch/sysio.epoch.abi @@ -126,6 +126,34 @@ "base": "", "fields": [] }, + { + "name": "roster_digest_entry", + "base": "", + "fields": [ + { + "name": "chain_code", + "type": "uint64" + }, + { + "name": "digest", + "type": "checksum256" + }, + { + "name": "sent_at_epoch", + "type": "uint32" + } + ] + }, + { + "name": "roster_digest_key", + "base": "", + "fields": [ + { + "name": "chain_code", + "type": "uint64" + } + ] + }, { "name": "schbatchgps", "base": "", @@ -214,6 +242,14 @@ "key_names": ["name"], "key_types": ["name"], "table_id": 59387 + }, + { + "name": "rosterdig", + "type": "roster_digest_entry", + "index_type": "i64", + "key_names": ["chain_code"], + "key_types": ["uint64"], + "table_id": 29555 } ], "ricardian_clauses": [], diff --git a/contracts/sysio.epoch/sysio.epoch.wasm b/contracts/sysio.epoch/sysio.epoch.wasm index bdcf872698fb9ca7a43aed8f09a2cf4caa48f85d..fcfde08d2e946cc6f160cec3b4261e1de39230c0 100755 GIT binary patch delta 21506 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 delta 21273 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 G?f(Ey-!W4F diff --git a/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp b/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp index 7a3e8b9aab..e3a7b9a198 100644 --- a/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp +++ b/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace sysio { @@ -96,6 +97,19 @@ namespace sysio { [[sysio::action]] void buildenv(uint64_t chain_code); + // ----------------------------------------------------------------------- + // Envelope budget + // ----------------------------------------------------------------------- + // + // ALIASES of the canonical values in `sysio.opp.common/opp_envelope_budget.hpp`, so the + // packing loop below can read them unqualified. They are not copies: the budget bounds + // what any attestation PRODUCER may emit, so `sysio.epoch` derives its roster ceiling + // from the same header rather than from this contract. + + static constexpr size_t MAX_ENVELOPE_BYTES = opp::MAX_ENVELOPE_BYTES; + static constexpr size_t ATTESTATION_OVERHEAD_BYTES = opp::ATTESTATION_OVERHEAD_BYTES; + static constexpr size_t ENVELOPE_BASELINE_BYTES = opp::ENVELOPE_BASELINE_BYTES; + // ----------------------------------------------------------------------- // Tables // ----------------------------------------------------------------------- diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index 919b049381..e779370330 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -55,33 +55,11 @@ using sysio::slug_name_literals::operator""_s; /// binds inbound registrations to this exact outpost — see the WSA-005 note there. constexpr sysio::slug_name NODE_OWNER_SRC_CHAIN = "ETHEREUM"_s; -/// Hard cap on the encoded envelope size in BOTH directions, mirroring the -/// Solana (`opp_outpost::MAX_ENVELOPE_BYTES`) and Ethereum (`OPP.MAX_ENVELOPE_BYTES`) -/// caps. 32 KiB is the e2e-supported maximum across WIRE / Ethereum / Solana. -/// Solana's 256 KiB BPF heap divided by ~3.3× envelope-size peak heap usage -/// during the finalising chunk's `Envelope::decode + keccak::hash + clone` -/// tolerates more, but Ethereum is the binding constraint: a cold -/// `emitOutboundEnvelope` of a near-64-KiB envelope costs ~45 M gas, ~2.7× the -/// EIP-7825 per-transaction cap of 16 777 216, so the platform cap is 32 768. -/// Outbound, the `buildenv` packing loop uses this to decide how many READY -/// attestations to bundle into the current epoch's envelope; any that don't fit -/// stay in the `attestations` table with status READY for the next epoch's -/// `buildenv` call. Inbound, `deliver` rejects anything larger before hashing -/// or storing it. -constexpr size_t MAX_ENVELOPE_BYTES = 32'768; - -/// Conservative per-attestation byte budget used by the `buildenv` packing -/// loop: protobuf tags + length prefixes + the attestation type/data-size -/// fields. Over-counts by a few bytes per attestation versus the actual -/// `zpp::bits` encoded size, which keeps the loop O(N) and always errs on the -/// side of leaving a gap. The trailing `packed.size()` check after final -/// serialisation is the hard backstop. -constexpr size_t ATTESTATION_OVERHEAD_BYTES = 24; - -/// Conservative envelope/message header budget for the packing loop — -/// covers the `Envelope` header fields, the wrapping `Message`, its header -/// + payload preamble, and a safety margin for `zpp::bits` length prefixes. -constexpr size_t ENVELOPE_BASELINE_BYTES = 512; +// MAX_ENVELOPE_BYTES / ATTESTATION_OVERHEAD_BYTES / ENVELOPE_BASELINE_BYTES are declared +// on the `msgch` contract class in sysio.msgch.hpp — the envelope budget bounds what any +// attestation producer may emit, so `sysio.epoch` derives its roster ceiling from them +// instead of restating the numbers. Every use below is inside a `msgch` member function +// and so still resolves unqualified. /// Stable audit marker for a UIC rejected before it can reach `rcrdcommit`. constexpr const char* UIC_DISPATCH_REJECTED_LOG_PREFIX = @@ -1723,10 +1701,25 @@ void msgch::buildenv(uint64_t chain_code) { // First-attestation-too-big guard. The estimator picks zero only when the first candidate alone // overshoots the envelope; the trim loop below would surface the same condition, but aborting - // upfront avoids building anything in the doomed case. Never expected at protocol level because - // every valid current attestation should fit by itself. - check(included_count > 0, - "sysio.msgch::buildenv: a single READY attestation exceeds the outbound envelope"); + // upfront avoids building anything in the doomed case. + // + // This abort is far more severe than "this outpost's envelope is skipped": `buildenv` is + // inline-sent from `sysio.epoch::advance`, so it takes the whole epoch-advance transaction with + // it and epoch advancement stops CHAIN-WIDE. The head-of-line `break` above makes it permanent + // rather than probabilistic — the same oversized candidate re-packs identically every epoch, and + // everything queued behind it never ships either. + // + // So the message carries the offending row's id and the arithmetic that rejected it, rather than + // being an anonymous overflow that has to be reconstructed from table state afterwards. The + // attestation type is readable from the `attestations` row the id names. + if (included_count == 0) { + const size_t head_bytes = ATTESTATION_OVERHEAD_BYTES + candidate_entries.front().data.size(); + check(false, + "sysio.msgch::buildenv: a single READY attestation exceeds the outbound envelope" + " -- att_id=" + std::to_string(candidate_ids.front()) + + " bytes=" + std::to_string(head_bytes) + + " budget=" + std::to_string(opp::SINGLE_ATTESTATION_BUDGET_BYTES)); + } std::vector entries( std::make_move_iterator(candidate_entries.begin()), diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index a5d866613a02d87a02ede15961d9d27957dcfb67..57a7c92f31022e645425d906094b8fba33f6e325 100755 GIT binary patch delta 16598 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 E52cJNZ~y=R delta 15039 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 diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/opp_envelope_budget.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/opp_envelope_budget.hpp new file mode 100644 index 0000000000..b381796651 --- /dev/null +++ b/contracts/sysio.opp.common/include/sysio.opp.common/opp_envelope_budget.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +/// OPP outbound-envelope budget. +/// +/// These live in `sysio.opp.common` rather than on the `sysio.msgch` contract that enforces +/// them because the budget bounds what any PRODUCER of an attestation may emit, not just the +/// packer that assembles the envelope. `sysio.epoch` derives its OPERATORS-roster ceiling from +/// them, and `sysio.opreg::setconfig` validates the registry ceilings against that — three +/// contracts, one set of numbers. A second copy of a consensus-visible cap is a drift hazard, +/// and the whole point of the cap is that every side agrees on it. +/// +/// `sysio.msgch` re-exposes them as members of its contract class so its own packing loop reads +/// them unqualified; those are aliases of these, never copies. +namespace sysio::opp { + +/// Hard cap on the encoded envelope size in BOTH directions, mirroring the +/// Solana (`opp_outpost::MAX_ENVELOPE_BYTES`) and Ethereum (`OPP.MAX_ENVELOPE_BYTES`) +/// caps. 32 KiB is the e2e-supported maximum across WIRE / Ethereum / Solana. +/// Solana's 256 KiB BPF heap divided by ~3.3× envelope-size peak heap usage +/// during the finalising chunk's `Envelope::decode + keccak::hash + clone` +/// tolerates more, but Ethereum is the binding constraint: a cold +/// `emitOutboundEnvelope` of a near-64-KiB envelope costs ~45 M gas, ~2.7× the +/// EIP-7825 per-transaction cap of 16 777 216, so the platform cap is 32 768. +/// Outbound, the `buildenv` packing loop uses this to decide how many READY +/// attestations to bundle into the current epoch's envelope; any that don't fit +/// stay in the `attestations` table with status READY for the next epoch's +/// `buildenv` call. Inbound, `deliver` rejects anything larger before hashing +/// or storing it. +inline constexpr size_t MAX_ENVELOPE_BYTES = 32'768; + +/// Conservative per-attestation byte budget used by the `buildenv` packing +/// loop: protobuf tags + length prefixes + the attestation type/data-size +/// fields. Over-counts by a few bytes per attestation versus the actual +/// `zpp::bits` encoded size, which keeps the loop O(N) and always errs on the +/// side of leaving a gap. The trailing `packed.size()` check after final +/// serialisation is the hard backstop. +inline constexpr size_t ATTESTATION_OVERHEAD_BYTES = 24; + +/// Conservative envelope/message header budget for the packing loop — +/// covers the `Envelope` header fields, the wrapping `Message`, its header +/// + payload preamble, and a safety margin for `zpp::bits` length prefixes. +inline constexpr size_t ENVELOPE_BASELINE_BYTES = 512; + +/// Bytes available to a SINGLE attestation that must ride an envelope alone — +/// the envelope cap less the header baseline and that attestation's own framing. +/// This is the figure `buildenv`'s first-attestation-too-big guard rejects against. +inline constexpr size_t SINGLE_ATTESTATION_BUDGET_BYTES = + MAX_ENVELOPE_BYTES - ENVELOPE_BASELINE_BYTES - ATTESTATION_OVERHEAD_BYTES; + +} // namespace sysio::opp diff --git a/contracts/sysio.opreg/src/sysio.opreg.cpp b/contracts/sysio.opreg/src/sysio.opreg.cpp index 46bb5d7f25..c7e815061f 100644 --- a/contracts/sysio.opreg/src/sysio.opreg.cpp +++ b/contracts/sysio.opreg/src/sysio.opreg.cpp @@ -191,6 +191,42 @@ bool is_fully_settled(const opreg::operator_entry& op) { return !has_active_locks(op.account); } +/// The `op_config` registration ceiling for `type`, or 0 when the type carries none. +/// +/// 0 is unambiguous as "uncapped": `setconfig` rejects any `max_available_*` that is +/// not positive, so a real ceiling is always >= 1. CHALLENGER has no `max_available_*` +/// field and is uncapped by construction — it is privileged-registered only. +uint32_t registration_ceiling_for_type(OperatorType type, const opreg::op_config& cfg) { + switch (type) { + case OperatorType::OPERATOR_TYPE_PRODUCER: return cfg.max_available_producers; + case OperatorType::OPERATOR_TYPE_BATCH: return cfg.max_available_batch_ops; + case OperatorType::OPERATOR_TYPE_UNDERWRITER: return cfg.max_available_underwriters; + default: return 0; + } +} + +/// Count NON-TERMINATED rows of `type`, stopping once `limit` is reached. +/// +/// The caller only needs to know whether the ceiling is already met, so the scan is +/// bounded by the ceiling rather than by the roster — registration stays O(ceiling) +/// however large the registry grows. +/// +/// TERMINATED rows are excluded so a terminated-and-settled operator frees its slot +/// when `prune` erases it; counting them would let settled history permanently exhaust +/// the registry. UNKNOWN, ACTIVE and SLASHED all count: UNKNOWN is what an attacker +/// accumulates for free, and ACTIVE + SLASHED is exactly the set the OPERATORS roster +/// ships. +uint32_t count_registered_of_type(name self, OperatorType type, uint32_t limit) { + opreg::operators_t ops(self); + auto type_idx = ops.get_index<"bytype"_n>(); + uint32_t count = 0; + for (auto it = type_idx.lower_bound(magic_enum::enum_integer(type)); + it != type_idx.end() && it->type == type && count < limit; ++it) { + if (it->status != OperatorStatus::OPERATOR_STATUS_TERMINATED) ++count; + } + return count; +} + } // anonymous namespace // --------------------------------------------------------------------------- @@ -220,6 +256,31 @@ void opreg::setconfig(uint32_t max_available_producers, "terminate_max_pct_misses_24h must be in [1, 99]"); check(terminate_window_ms > 0, "terminate_window_ms must be positive"); + // The registry ceilings bound the OPERATORS attestation, so they cannot be raised + // past what an outbound envelope can carry. + // + // Every registered operator rides that attestation until it is TERMINATED, and it is + // the one attestation whose size grows with adoption — so a governance change that + // raised these three fields without this check could make `sysio.epoch::advance` + // produce a roster no envelope can hold. `buildenv` would then abort on its + // single-attestation guard, and because it is inline-sent from `advance` that stops + // epoch advancement CHAIN-WIDE, permanently, with no configuration left to change it + // through. `sysio.epoch` already applies this reasoning to the sibling + // BATCH_OPERATOR_GROUPS attestation via `MAX_SCHEDULED_BATCH_OPERATORS`. + // + // The sum is what matters: all three types share one roster. Bootstrapped operators + // bypass the per-type registration ceiling, and CHALLENGER carries none, so the + // headroom `epoch::MAX_ROSTER_OPERATORS` reserves (half an envelope) also absorbs + // those. + { + const uint64_t total_ceiling = static_cast(max_available_producers) + + max_available_batch_ops + + max_available_underwriters; + check(total_ceiling <= sysio::epoch::MAX_ROSTER_OPERATORS, + "max_available_* sum exceeds the OPERATORS roster ceiling: the resulting " + "roster could not fit an outbound envelope"); + } + // SEC-28 residual: delivery records accrue only on duty epochs -- one per // `batch_op_groups`-epoch rotation for a resident operator -- so a rolling // window narrower than the full consecutive-miss run of duty epochs makes @@ -328,6 +389,27 @@ void opreg::regoperator(name account, ops.erase(op_pk); } + // Registration ceiling. `op_config.max_available_*` has been declared, validated in + // `setconfig` and persisted since the registry landed, but was read by NOTHING — so + // the registry grew without any count, rate or admission bound, and registration is + // self-authorizing with the row billed to the sysio RAM pool rather than to the + // registrant. Enforcing the ceiling here is what makes that bound real, and what + // stops the OPERATORS roster (sized against the outbound envelope) from growing + // without limit. + // + // Bootstrapped operators bypass it, consistently with their other privileged paths + // (`bootstrapped-operator-invariants.md`): they are the genesis seed the chain cannot + // start without, and they are privileged-registered. + if (!is_bootstrapped) { + opconfig_t ceiling_cfg_tbl(get_self()); + const auto ceiling_cfg = ceiling_cfg_tbl.get_or_default(op_config{}); + const auto ceiling = registration_ceiling_for_type(type, ceiling_cfg); + if (ceiling > 0) { + check(count_registered_of_type(get_self(), type, ceiling) < ceiling, + "operator registration ceiling reached for this operator type"); + } + } + // Verify authex links exist for all active outpost chains. // Skip when: bootstrapped OR privileged caller (sysio.opreg registering on behalf) // diff --git a/contracts/sysio.opreg/sysio.opreg.wasm b/contracts/sysio.opreg/sysio.opreg.wasm index cc546babd4540862f7c9a8840c1a7709ed158029..48378c0e9a1bb45355ed1363679d7096b41afa74 100755 GIT binary patch delta 12854 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 h7Hgh$F1vD1c#0W4X_)qT>2%aSzhzW_oy`#Asr delta 11430 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 ZosN}gt~ uws; for (uint32_t i = 0; i < 33; ++i) { std::string s = "uwcap"; @@ -5981,4 +5994,152 @@ BOOST_FIXTURE_TEST_CASE(lock_hold_actions_require_chalg_auth, sysio_uwchal_teste .find("missing authority of sysio.chalg") != std::string::npos); } FC_LOG_AND_RETHROW() } +// --------------------------------------------------------------------------- +// WIRE-342 — the OPERATORS roster ships only when its content changed +// --------------------------------------------------------------------------- +// +// `next_att_id()` is the monotonic `attseq` counter every `queueout` mints from, so its +// delta across one `advance` counts what that advance queued — and unlike the +// `attestations` rows themselves it survives the inline `buildenv` drain. The assertions +// below are RELATIVE (static-epoch delta vs roster-changing-epoch delta) so they stay +// valid whatever else `advance` queues per epoch. + +/// Bring the fixture to a state where `advance` actually COMPLETES. +/// +/// `bootstrap_for_dispatch` alone is not enough: its own advance runs while emissions are +/// unconfigured, so `sysio.epoch::advance` gate-blocks and returns at its emissions check — +/// long before the roster/queueout block. Every delta below would then be 0, and a test +/// asserting "no roster was queued" would pass for entirely the wrong reason. +#define REQUIRE_ADVANCING_FIXTURE() \ + do { \ + bootstrap_for_dispatch(); \ + setup_wire_token_and_reserves(); \ + enable_epoch_advancement(); \ + BOOST_REQUIRE_EQUAL(success(), \ + push(MSGCH_ACCOUNT, msgch_abi, MSGCH_ACCOUNT, "bootstrap"_n, mvo())); \ + produce_blocks(); \ + BOOST_REQUIRE_EQUAL(current_epoch(), 1u); \ + } while (0) + +BOOST_FIXTURE_TEST_CASE(advance_queues_operators_only_when_the_roster_changes, + sysio_dispatch_tester) { try { + REQUIRE_ADVANCING_FIXTURE(); + create_accounts({"batchop.b"_n}); + produce_blocks(); + + // Establish the per-epoch baseline over an unchanged roster. + const uint32_t epoch_before_baseline = current_epoch(); + age_one_epoch(); + const uint64_t before_first = next_att_id(); + age_one_epoch(); + const uint64_t static_delta = next_att_id() - before_first; + + // Non-vacuity guards. Without these the whole test passes when `advance` silently does + // nothing — which is exactly how the first version of it "passed". + BOOST_REQUIRE_GT(current_epoch(), epoch_before_baseline); + BOOST_REQUIRE_GT(static_delta, 0u); + + // A second unchanged epoch must queue exactly the same set — the roster re-derives to + // byte-identical content, so no OPERATORS attestation is minted. Before this change the + // full roster was re-encoded and fanned to every outpost on EVERY advance regardless. + const uint64_t before_second = next_att_id(); + age_one_epoch(); + BOOST_REQUIRE_EQUAL(static_delta, next_att_id() - before_second); + + // A bootstrapped registration lands ACTIVE immediately, so the roster's content changes + // and the next advance queues exactly one MORE attestation: the roster itself. + BOOST_REQUIRE_EQUAL(success(), push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, + "regoperator"_n, mvo() + ("account", std::string("batchop.b")) + ("type", OperatorType::OPERATOR_TYPE_BATCH) + ("is_bootstrapped", true))); + + const uint64_t before_change = next_att_id(); + age_one_epoch(); + BOOST_REQUIRE_EQUAL(static_delta + 1, next_att_id() - before_change); + + // And the epoch after the change is static again — one send per change, not a latch. + const uint64_t before_after = next_att_id(); + age_one_epoch(); + BOOST_REQUIRE_EQUAL(static_delta, next_att_id() - before_after); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(advance_ignores_never_bonded_registrations, sysio_dispatch_tester) { try { + REQUIRE_ADVANCING_FIXTURE(); + create_accounts({"batchop.b"_n}); + produce_blocks(); + + age_one_epoch(); + const uint64_t before_first = next_att_id(); + age_one_epoch(); + const uint64_t static_delta = next_att_id() - before_first; + + // This assertion is what makes the test meaningful: it proves advance is really queueing + // per epoch, so the "no extra attestation" check below is a genuine observation rather + // than a comparison of two zeroes. + BOOST_REQUIRE_GT(static_delta, 0u); + + // A NON-bootstrapped registration lands OPERATOR_STATUS_UNKNOWN — registered, never + // bonded. It is excluded from the roster, so it changes no content, so it produces NO + // envelope traffic whatsoever. + // + // This is the whole WIRE-342 attack closure: accumulating never-bonded registrations + // was free (self-authorizing, row billed to the sysio RAM pool) and every one of them + // rode every envelope to every outpost. Now it costs the registrant a transaction and + // the chain nothing at all. + BOOST_REQUIRE_EQUAL(success(), push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, + "regoperator"_n, mvo() + ("account", std::string("batchop.b")) + ("type", OperatorType::OPERATOR_TYPE_BATCH) + ("is_bootstrapped", false))); + + const uint64_t before_unknown = next_att_id(); + age_one_epoch(); + BOOST_REQUIRE_EQUAL(static_delta, next_att_id() - before_unknown); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(advance_sends_the_roster_to_a_newly_activated_outpost_only, + sysio_dispatch_tester) { try { + REQUIRE_ADVANCING_FIXTURE(); + + age_one_epoch(); + const uint64_t before_first = next_att_id(); + age_one_epoch(); + const uint64_t static_delta = next_att_id() - before_first; // one outpost + BOOST_REQUIRE_GT(static_delta, 0u); + + // Register a SECOND outpost. The roster's content is unchanged — it lists operators, + // not chains — so the incumbent outpost's digest still matches and it gets nothing. + // The new outpost has no digest row at all, and that ABSENCE is what makes it receive + // the roster on its very first advance. + BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, + "regchain"_n, mvo() + ("kind", ChainKind::CHAIN_KIND_SVM) + ("code", codename_mvo("SOL")) + ("external_chain_id", 900) + ("name", std::string("outpost-two")) + ("description", std::string{}))); + + // `regchain` outside the bootstrap window inserts the row with `active = false`, and + // `is_active_outpost` skips it — so no fan-out reaches it until it is activated. That + // gate is exactly what makes "a newly ACTIVATED outpost" the event under test here. + BOOST_REQUIRE_EQUAL(success(), push(CHAINS_ACCOUNT, chains_abi, CHAINS_ACCOUNT, + "activchain"_n, mvo()("code", codename_mvo("SOL")))); + produce_blocks(); + + // +1 for the second outpost's own BATCH_OPERATOR_GROUPS (which ships every epoch to + // every outpost) and +1 for the roster going to the NEW outpost alone. If the digest + // gate were global rather than per-outpost this would be +1; if it re-sent to every + // outpost on any miss it would be +3. + const uint64_t before_new = next_att_id(); + age_one_epoch(); + BOOST_REQUIRE_EQUAL(static_delta + 2, next_att_id() - before_new); + + // Both outposts now hold the same roster digest, so the next epoch carries only the + // two per-epoch group attestations. + const uint64_t before_settled = next_att_id(); + age_one_epoch(); + BOOST_REQUIRE_EQUAL(static_delta + 1, next_att_id() - before_settled); +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_SUITE_END() diff --git a/contracts/tests/sysio.opreg_tests.cpp b/contracts/tests/sysio.opreg_tests.cpp index 7e22e3b613..e64e5e1315 100644 --- a/contracts/tests/sysio.opreg_tests.cpp +++ b/contracts/tests/sysio.opreg_tests.cpp @@ -1455,4 +1455,98 @@ BOOST_FIXTURE_TEST_CASE(flushwtdw_bounds_rows_per_epoch, sysio_opreg_tester) { t BOOST_REQUIRE_EQUAL(0u, count_pending()); } FC_LOG_AND_RETHROW() } +// --------------------------------------------------------------------------- +// WIRE-342 — registry ceilings bound the OPERATORS roster +// --------------------------------------------------------------------------- + +/// `epoch::MAX_ROSTER_OPERATORS` mirrored for the host tests: the epoch contract header +/// is CDT-only and cannot be included here, so the derivation is restated with its +/// inputs rather than as a bare number. If `sysio.epoch.hpp` changes any of these, this +/// constant diverges and the boundary case below fails — which is the intent: the +/// ceiling is consensus-visible and a silent change to it should not pass unnoticed. +/// +/// (32768 envelope - 512 baseline - 24 overhead) / 2 share / 102 bytes-per-operator +static constexpr uint32_t EXPECTED_MAX_ROSTER_OPERATORS = (32768 - 512 - 24) / 2 / 102; + +BOOST_FIXTURE_TEST_CASE(setconfig_accepts_shipped_default_ceilings, sysio_opreg_tester) { try { + // The defaults must remain a legal configuration — a ceiling that rejected them would + // brick `setconfig` for every existing cluster. + BOOST_REQUIRE_EQUAL(success(), setconfig(21, 63, 21)); + BOOST_REQUIRE_LT(21u + 63u + 21u, EXPECTED_MAX_ROSTER_OPERATORS); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(setconfig_rejects_ceilings_exceeding_roster_capacity, sysio_opreg_tester) { try { + // Exactly at the ceiling is legal; one over is not. Pinning both sides is what makes + // this a boundary test rather than a smoke test. + const uint32_t at_ceiling = EXPECTED_MAX_ROSTER_OPERATORS; + BOOST_REQUIRE_EQUAL(success(), setconfig(at_ceiling - 2, 1, 1)); + + BOOST_REQUIRE_EQUAL( + error("assertion failure with message: max_available_* sum exceeds the OPERATORS " + "roster ceiling: the resulting roster could not fit an outbound envelope"), + setconfig(at_ceiling - 1, 1, 1)); + + // A governance-scale raise is the case this guard exists for: without it the roster + // could grow past what an envelope carries, and `buildenv` aborts inline inside + // `sysio.epoch::advance` — halting epoch advancement chain-wide with no config left + // to change it through. + BOOST_REQUIRE_EQUAL( + error("assertion failure with message: max_available_* sum exceeds the OPERATORS " + "roster ceiling: the resulting roster could not fit an outbound envelope"), + setconfig(500, 500, 500)); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(regoperator_enforces_per_type_ceiling, sysio_opreg_tester) { try { + // One batch-operator slot. `max_available_*` was declared, validated and persisted + // since the registry landed but read by NOTHING, so registration had no count, rate + // or admission bound at all. + BOOST_REQUIRE_EQUAL(success(), setconfig(21, 1, 21)); + + BOOST_REQUIRE_EQUAL(success(), + regoperator("batchop.a"_n, OperatorType::OPERATOR_TYPE_BATCH, false)); + + BOOST_REQUIRE_EQUAL( + error("assertion failure with message: operator registration ceiling reached for " + "this operator type"), + regoperator("batchop.b"_n, OperatorType::OPERATOR_TYPE_BATCH, false)); + + // The ceiling is PER TYPE — a full batch-operator roster must not block underwriters. + BOOST_REQUIRE_EQUAL(success(), + regoperator("uwrit.a"_n, OperatorType::OPERATOR_TYPE_UNDERWRITER, false)); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(regoperator_ceiling_bypassed_by_bootstrapped, sysio_opreg_tester) { try { + // Bootstrapped operators are the genesis seed the chain cannot start without, and are + // already exempt from the collateral and termination gates + // (`bootstrapped-operator-invariants.md`). The ceiling must not be the one gate that + // can refuse them, or a mis-set config could make the chain unstartable. + BOOST_REQUIRE_EQUAL(success(), setconfig(21, 1, 21)); + + BOOST_REQUIRE_EQUAL(success(), + regoperator("batchop.a"_n, OperatorType::OPERATOR_TYPE_BATCH, false)); + BOOST_REQUIRE_EQUAL(success(), + regoperator("batchop.b"_n, OperatorType::OPERATOR_TYPE_BATCH, true)); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(regoperator_ceiling_ignores_terminated_rows, sysio_opreg_tester) { try { + // The count deliberately excludes TERMINATED rows. Counting them would let settled + // history permanently exhaust the registry: terminated rows are erased only by the + // capped, permissionless `prune`, so a chain with churn would eventually refuse every + // registration. UNKNOWN, ACTIVE and SLASHED all count — UNKNOWN is what an attacker + // accumulates for free, and ACTIVE + SLASHED is exactly what the roster ships. + BOOST_REQUIRE_EQUAL(success(), setconfig(21, 1, 21)); + + BOOST_REQUIRE_EQUAL(success(), + regoperator("batchop.a"_n, OperatorType::OPERATOR_TYPE_BATCH, false)); + BOOST_REQUIRE_EQUAL( + error("assertion failure with message: operator registration ceiling reached for " + "this operator type"), + regoperator("batchop.b"_n, OperatorType::OPERATOR_TYPE_BATCH, false)); + + // Terminating the incumbent frees the slot even before `prune` erases the row. + BOOST_REQUIRE_EQUAL(success(), terminate("batchop.a"_n, "test")); + BOOST_REQUIRE_EQUAL(success(), + regoperator("batchop.b"_n, OperatorType::OPERATOR_TYPE_BATCH, false)); +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_SUITE_END() From 83c621e2fb95111f295540f037569bce575edbf6 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 18 Aug 2026 08:12:43 -0500 Subject: [PATCH 2/3] test(epoch): guard the roster's SLASHED carve-out `advance` builds the OPERATORS roster from ACTIVE + SLASHED. The SLASHED half is load-bearing and had no test: `sysio.opreg::slash` flips the row to SLASHED before emitting OPERATOR_ACTION(SLASH), and the Solana slash handler resolves its target THROUGH the roster the outpost holds -- so narrowing the filter to ACTIVE-only would turn every subsequent slash into a silent no-op on the outpost rather than a loud failure. Verified by mutation: dropping OPERATOR_STATUS_SLASHED from the scan fails this case. Asserts against the outbound ENVELOPE rather than the `attestations` rows, because `buildenv` consumes those rows as it bundles them -- they no longer exist once an advance returns. The envelope is also the stronger surface: it is the exact payload the outpost consumes. Test-only; no contract source, .wasm or .abi changes. Change-Id: I00e7bb6a0005ac4b8e8c9d8886976811fa2cf4dd --- contracts/tests/sysio.dispatch_tests.cpp | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 3a0e99c867..211032af2e 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -1020,6 +1020,24 @@ class sysio_dispatch_tester : public tester { return v["next"].as_uint64(); } + /// Raw bytes of the outbound envelope currently staged for `chain_code`. `sysio.msgch` + /// keeps `outenvelopes` one row deep per outpost and replaces it on each `buildenv`, so + /// this returns the most recent epoch's bytes -- the exact payload the outpost consumes. + /// Reading the ENVELOPE rather than the `attestations` rows is deliberate: `buildenv` + /// consumes the READY attestation rows as it bundles them, so they no longer exist by + /// the time an advance returns. + std::vector outbound_envelope_bytes(uint64_t chain_code, uint64_t scan_until = 32) { + for (uint64_t id = 0; id < scan_until; ++id) { + auto data = get_row_by_id(MSGCH_ACCOUNT, MSGCH_ACCOUNT, "outenvelopes"_n, id); + if (data.empty()) continue; + auto row = msgch_abi.binary_to_variant("outbound_envelope", data, + abi_serializer::create_yield_function(abi_serializer_max_time)); + if (row["chain_code"].as_uint64() != chain_code) continue; + return row["raw_envelope"].as>(); + } + return {}; + } + /// Read a collateral lock row by lock_id (uwrit `locks` KV table). lock_ids /// are allocated from 1 (uwcounters default), so the first swap's source + /// destination locks are ids 1 and 2. @@ -6142,4 +6160,45 @@ BOOST_FIXTURE_TEST_CASE(advance_sends_the_roster_to_a_newly_activated_outpost_on BOOST_REQUIRE_EQUAL(static_delta + 1, next_att_id() - before_settled); } FC_LOG_AND_RETHROW() } +BOOST_FIXTURE_TEST_CASE(advance_roster_retains_slashed_operators, sysio_dispatch_tester) { try { + REQUIRE_ADVANCING_FIXTURE(); + create_accounts({"batchop.b"_n}); + produce_blocks(); + + // A bootstrapped registration lands ACTIVE immediately, so it joins the roster at once. + BOOST_REQUIRE_EQUAL(success(), push(OPREG_ACCOUNT, opreg_abi, OPREG_ACCOUNT, + "regoperator"_n, mvo() + ("account", std::string("batchop.b")) + ("type", OperatorType::OPERATOR_TYPE_BATCH) + ("is_bootstrapped", true))); + + // Settle the roster so the operator's ACTIVE entry is what the recorded digest covers. + age_one_epoch(); + + // `sysio.opreg::slash` flips the row to SLASHED *before* emitting OPERATOR_ACTION(SLASH). + BOOST_REQUIRE_EQUAL(success(), slash_op("batchop.b"_n, "wire-342 roster retention")); + + const uint64_t before_slash_epoch = next_att_id(); + age_one_epoch(); + + // `status` is part of the encoded entry, so the flip is a content change and the digest + // gate must let this roster through rather than suppressing it as unchanged. Guarding on + // the mint counter first keeps the payload assertion below from passing against a stale + // envelope that some earlier epoch happened to leave staged. + BOOST_REQUIRE_GT(next_att_id(), before_slash_epoch); + + // Read the ENVELOPE, not the attestation rows: `buildenv` consumes those as it bundles. + const auto raw = outbound_envelope_bytes(fc::slug_name{"ETH"}.value); + BOOST_REQUIRE(!raw.empty()); + + // THE REGRESSION GUARD for the SLASHED carve-out. `build_operator_entry` writes + // `entry.account.name` as a plain string, so the account name appears verbatim in the + // serialized bytes. Narrowing the roster filter to ACTIVE-only would drop this entry -- + // and because the Solana slash handler resolves its target THROUGH the roster the + // outpost holds, that would silently turn every subsequent slash into a no-op on the + // outpost instead of failing loudly. + const std::string encoded(raw.begin(), raw.end()); + BOOST_REQUIRE(encoded.find("batchop.b") != std::string::npos); +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_SUITE_END() From 572b203bf125b996a2f45929ee0fdc88fe1a4863 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 18 Aug 2026 10:53:53 -0500 Subject: [PATCH 3/3] WIRE-342: bound the roster by the outpost that has to seat it Review findings from huangminghuang on #568. **The ceiling was derived from bytes alone, which is necessary but not sufficient.** An outpost that cannot seat the roster it receives skips the WHOLE attestation and retains its previous one, so the roster silently stops tracking the depot. Solana's `OperatorRegistry` is a fixed-size PDA -- 32 entries on `next`, raised to 128 by wire-solana#442 (SOL-385), which explicitly defers the depot-side number here. Ethereum applies no count cap, so Solana is the binding one. `MAX_ROSTER_OPERATORS` (a fixed 158) is replaced by `max_roster_operators(outpost_count)` = min(envelope capacity, 128 entries). **The bytes half was also wrong the moment a third outpost registers.** `ROSTER_BYTES_PER_OPERATOR` was pinned to the two-address shape while the address walk allows eight, so the derivation silently overstated capacity as outposts grow. It is now parameterised on the live active-outpost count, since an operator carries one address per registered outpost. At the launch shape (2) the ENTRY cap binds at 128; at 3 it is 114 and at 4 it is 89, where bytes take over. **The configured sum was not a safety invariant.** Bootstrapped registrations land ACTIVE immediately -- so they are in the roster at once -- yet bypassed the per-type ceilings entirely. `regoperator` now applies a global ACTIVE+SLASHED roster bound to EVERY registration. Bootstrap's exemptions from collateral and termination are economic (`bootstrapped-operator-invariants.md`); exceeding a transport/seating ceiling is not a cost to the operator, it wedges the outpost for everyone. `is_active_outpost` moves to the chains header: it was already duplicated between `sysio.epoch.cpp` and `sysio.opreg.cpp`, and the ceiling derivation would have been a third copy. `active_outpost_count()` joins it as the shared census. 658/658 contract unit tests. Change-Id: I110c2f2eee572330d6b78ce87ba7ba33ebe5fb1c --- .../include/sysio.chains/sysio.chains.hpp | 24 +++++ .../include/sysio.epoch/sysio.epoch.hpp | 58 +++++++---- contracts/sysio.epoch/sysio.epoch.wasm | Bin 76106 -> 76115 bytes contracts/sysio.opreg/src/sysio.opreg.cpp | 55 ++++++++-- contracts/sysio.opreg/sysio.opreg.wasm | Bin 92142 -> 94383 bytes contracts/tests/sysio.opreg_tests.cpp | 94 ++++++++++++++++-- 6 files changed, 195 insertions(+), 36 deletions(-) diff --git a/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp b/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp index f001677332..47b6a221e8 100644 --- a/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp +++ b/contracts/sysio.chains/include/sysio.chains/sysio.chains.hpp @@ -104,6 +104,30 @@ namespace sysio { sysio::kv::index<"byextid"_n, sysio::const_mem_fun>, sysio::kv::index<"byactive"_n, sysio::const_mem_fun> >; + + /// True when a chains row represents an active OUTPOST — i.e. it is active and + /// is not the depot self-row (which carries no outpost). + /// + /// Shared because three call sites need the identical predicate: `sysio.epoch`'s + /// envelope fan-out loops, `sysio.opreg::regoperator`'s authex-link requirement, + /// and the roster-ceiling derivation. Divergence between them would mean the + /// depot sizes a roster against one outpost set while fanning it to another. + static inline bool is_active_outpost(const chain_row& row) { + return row.active && !row.is_depot; + } + + /// Number of active outposts. This is the number of chain addresses a fully + /// bonded operator carries, since every operator type must bond on every + /// registered outpost (see `outpost-three-concerns.md`), which is what makes it + /// the correct multiplier for the OPERATORS roster's bytes-per-operator. + static inline uint32_t active_outpost_count() { + chains_t chains_tbl(name{"sysio.chains"_n}); + uint32_t count = 0; + for (auto it = chains_tbl.begin(); it != chains_tbl.end(); ++it) { + if (is_active_outpost(*it)) ++count; + } + return count; + } }; } // namespace sysio diff --git a/contracts/sysio.epoch/include/sysio.epoch/sysio.epoch.hpp b/contracts/sysio.epoch/include/sysio.epoch/sysio.epoch.hpp index b151f55e27..25adf694cf 100644 --- a/contracts/sysio.epoch/include/sysio.epoch/sysio.epoch.hpp +++ b/contracts/sysio.epoch/include/sysio.epoch/sysio.epoch.hpp @@ -239,23 +239,23 @@ namespace sysio { // `max_available_*` ceilings against the result, so a governance change cannot // raise the registry past what an envelope can carry. - /// Chain addresses carried per operator at the REALISTIC shape: one per - /// registered outpost. The launch outpost set is {ETH, SOL}, and every operator - /// type must bond on every registered outpost, so two is the shape to size - /// against — not `MAX_OPERATOR_CHAIN_ADDRESSES`, which is the defensive per-entry - /// cap for a pathological account, not the expected census. - static constexpr uint32_t ROSTER_ADDRESSES_PER_OPERATOR = 2; - /// Encoded bytes of one `ChainAddress`: 1 B tag + 1 B length + 2 B `kind` varint /// + 2 B inner tag/length + a 33-byte compressed secp256k1 key (Ed25519 is 32, /// so 33 is the worst case). static constexpr uint32_t ROSTER_BYTES_PER_ADDRESS = 39; - /// Encoded bytes of one `OperatorEntry`: a maximum-length WIRE account name - /// (4 B framing + 13 B) + its addresses + 2 B `type` + 3 B `status` (SLASHED is - /// 241, a two-byte varint) + 2 B for the repeated-field framing in `Operators`. - static constexpr uint32_t ROSTER_BYTES_PER_OPERATOR = - 17 + ROSTER_ADDRESSES_PER_OPERATOR * ROSTER_BYTES_PER_ADDRESS + 2 + 3 + 2; + /// Encoded bytes of one `OperatorEntry` carrying `addresses` chain addresses: + /// a maximum-length WIRE account name (4 B framing + 13 B) + its addresses + /// + 2 B `type` + 3 B `status` (SLASHED is 241, a two-byte varint) + 2 B for the + /// repeated-field framing in `Operators`. + /// + /// Parameterised on the address count rather than pinned to today's outpost set: + /// an operator carries one address per registered outpost, so registering a third + /// outpost makes every entry ~39 B fatter. A fixed constant would silently + /// overstate capacity the moment that happens. + static constexpr uint32_t roster_bytes_per_operator(uint32_t addresses) { + return 17 + addresses * ROSTER_BYTES_PER_ADDRESS + 2 + 3 + 2; + } /// The share of one envelope the roster may claim. /// @@ -267,12 +267,34 @@ namespace sysio { /// traffic that settles user value, which must not be deferred behind the roster. static constexpr uint32_t ROSTER_ENVELOPE_SHARE_DIVISOR = 2; - /// Ceiling on the total registered (non-bootstrapped) operator count, derived - /// from the envelope budget that `sysio.msgch` owns rather than restating it. - static constexpr uint32_t MAX_ROSTER_OPERATORS = - static_cast(sysio::opp::SINGLE_ATTESTATION_BUDGET_BYTES - / ROSTER_ENVELOPE_SHARE_DIVISOR - / ROSTER_BYTES_PER_OPERATOR); + /// Strictest OUTPOST-side ceiling on roster ENTRIES. + /// + /// The depot's envelope arithmetic is necessary but not sufficient: an outpost + /// that cannot seat the roster it receives skips the WHOLE attestation and keeps + /// its previous one, so the roster silently stops tracking the depot. The Solana + /// outpost's `OperatorRegistry` is a fixed-size PDA — 32 entries today, raised to + /// 128 by wire-solana#442 (SOL-385), which explicitly defers the depot-side number + /// to WIRE-342. Ethereum applies no count cap, so Solana is the binding one. + /// + /// Keep in lock-step with `liqsol-core`'s `MAX_OPERATORS`; this bound is only + /// meaningful while it is the smaller of the two. + static constexpr uint32_t MAX_OUTPOST_ROSTER_ENTRIES = 128; + + /// Roster ceiling for a network carrying `outpost_count` active outposts: the + /// smaller of what half an envelope can carry and what the strictest outpost can + /// seat. Both bounds are real and neither implies the other — bytes gate what the + /// depot can SEND, entries gate what an outpost can STORE. + static constexpr uint32_t max_roster_operators(uint32_t outpost_count) { + const uint32_t addresses = + outpost_count == 0 ? 1 + : (outpost_count > MAX_OPERATOR_CHAIN_ADDRESSES + ? MAX_OPERATOR_CHAIN_ADDRESSES : outpost_count); + const uint32_t by_bytes = + static_cast(sysio::opp::SINGLE_ATTESTATION_BUDGET_BYTES + / ROSTER_ENVELOPE_SHARE_DIVISOR + / roster_bytes_per_operator(addresses)); + return by_bytes < MAX_OUTPOST_ROSTER_ENTRIES ? by_bytes : MAX_OUTPOST_ROSTER_ENTRIES; + } private: diff --git a/contracts/sysio.epoch/sysio.epoch.wasm b/contracts/sysio.epoch/sysio.epoch.wasm index fcfde08d2e946cc6f160cec3b4261e1de39230c0..d532e02951c47c2b57856c51acbe444e40f329f8 100755 GIT binary patch delta 515 zcmXw#&r1|x7{~q2%xa2GBblz|{!m-@l#m8l5w;+7@nDIN;;Dnk9F$1t5+z;w1D;@U zg;fYWn1|x6mhPRFJh(=|YBRgDqq~i3YpCF%L(dHEdw9N|_xn80%lka(jIy0k-v5+a z<)EydA4%$vR8>-0=urNLyl|9vmyoBIoniXyA7H8k{6cDfE~CKa7{^MWPayCK_{~O$ zegtnYaTS)u)lg$CErgqBf4Yr2dS}I#NdG*tR9?K!F_;*|j+jfF$$Xhfx>h22A3J0v zW7wswl|J!s`3121c5B7Q$;_%-qO7@H<7>V_@pbPF@QWsjk#+-N#2R&N!&Ob!OjkQu zDPH`DoERxBNHl3YfJGZdPt0=DR*kNCU{_(k3Z*KP8N4@-s&L|^2;VPL#GHps39}!2 zv}r!0dxaKylv|}+b`v$^yHOI?^22C$&j04AU)LOZWj)3L9c8beOehTDkYa^1{Gr?S z2rA;WeHZl2NyCWGwPKWt2SYqTwEWZuQ!M>eC6@K$o2d7OzMyM4a!#JGIdz&=k&QM%$8KC4!zu;y4Zj_q5ph)$?pIF delta 530 zcmXw#O=uHQ6oox+ruK()oT9Xo&@_RX28s=}t+t>-3xX~!q6-lh6)mKzqM!>w-MZ=G zN&{J>inW_|Av`sLxYAXY5q~h6#%3~2XOhM?E!5(|J45pp_k8cXdoGXrIjd%}>Z@-` zr^ZK!fZ(dJC_RenP*wSd@6uM6kKzYC><-ggW0Aws~$wRU%JuuB{zCwscDR6ij&CEhhpz|4q;5J9g6E7m<_NR zP;NklVa09!(|{fKiNfX+{Gw%Z2zknz_pnM=OYcyiBUViO25o%|%Ij7iI*M-A7uP*C zSc%rBgo(tYKtPm0kGK7Fq8yHnmSY&+^t=^25xL%|B p&Mcd^b~Bo{Gb3?Pg&!M(); + uint32_t count = 0; + for (const auto status : { OperatorStatus::OPERATOR_STATUS_ACTIVE, + OperatorStatus::OPERATOR_STATUS_SLASHED }) { + for (auto it = status_idx.lower_bound(magic_enum::enum_integer(status)); + it != status_idx.end() && it->status == status && count < limit; ++it) { + ++count; + } + } + return count; +} + uint32_t count_registered_of_type(name self, OperatorType type, uint32_t limit) { opreg::operators_t ops(self); auto type_idx = ops.get_index<"bytype"_n>(); @@ -268,17 +285,22 @@ void opreg::setconfig(uint32_t max_available_producers, // through. `sysio.epoch` already applies this reasoning to the sibling // BATCH_OPERATOR_GROUPS attestation via `MAX_SCHEDULED_BATCH_OPERATORS`. // - // The sum is what matters: all three types share one roster. Bootstrapped operators - // bypass the per-type registration ceiling, and CHALLENGER carries none, so the - // headroom `epoch::MAX_ROSTER_OPERATORS` reserves (half an envelope) also absorbs - // those. + // The sum is what matters: all three types share one roster. CHALLENGER carries no + // per-type ceiling, but a self-registered challenger can never reach the roster: ACTIVE + // at registration requires `is_bootstrapped` (privileged), and the eligibility path + // returns false for CHALLENGER, so it stays UNKNOWN and the ACTIVE+SLASHED filter + // excludes it. Bootstrapped operators DO reach the roster, so they are bounded by the + // global actual-roster check in `regoperator` rather than by this configured sum. { const uint64_t total_ceiling = static_cast(max_available_producers) + max_available_batch_ops + max_available_underwriters; - check(total_ceiling <= sysio::epoch::MAX_ROSTER_OPERATORS, - "max_available_* sum exceeds the OPERATORS roster ceiling: the resulting " - "roster could not fit an outbound envelope"); + const uint32_t roster_ceiling = + sysio::epoch::max_roster_operators(sysio::chains::active_outpost_count()); + check(total_ceiling <= roster_ceiling, + "max_available_* sum exceeds the OPERATORS roster ceiling (" + + std::to_string(roster_ceiling) + "): the resulting roster could not fit an " + "outbound envelope, or could not be seated by an outpost"); } // SEC-28 residual: delivery records accrue only on duty epochs -- one per @@ -410,6 +432,25 @@ void opreg::regoperator(name account, } } + // Global actual-roster bound — applies to EVERY registration, bootstrapped included. + // + // The per-type ceilings above are a CONFIGURED sum that the bootstrap path bypasses, + // so on their own they are not a safety invariant: a privileged seed could push the + // LIVE roster past what half an envelope carries or what an outpost can seat, and a + // bootstrapped operator lands ACTIVE immediately, so it is in the roster at once. + // Bootstrap's exemptions from collateral and termination are ECONOMIC + // (`bootstrapped-operator-invariants.md`); they do not extend to a consensus-liveness + // capacity limit, because exceeding it does not harm the operator — it wedges the + // outpost for everyone. + { + const uint32_t roster_ceiling = + sysio::epoch::max_roster_operators(sysio::chains::active_outpost_count()); + check(count_roster_operators(get_self(), roster_ceiling) < roster_ceiling, + "OPERATORS roster ceiling reached (" + std::to_string(roster_ceiling) + + "): registering another operator would produce a roster that cannot fit an " + "outbound envelope or be seated by an outpost"); + } + // Verify authex links exist for all active outpost chains. // Skip when: bootstrapped OR privileged caller (sysio.opreg registering on behalf) // diff --git a/contracts/sysio.opreg/sysio.opreg.wasm b/contracts/sysio.opreg/sysio.opreg.wasm index 48378c0e9a1bb45355ed1363679d7096b41afa74..771541971e88fb7358eab79c9c36ecf3c7fde4b9 100755 GIT binary patch delta 19975 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 delta 17908 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 diff --git a/contracts/tests/sysio.opreg_tests.cpp b/contracts/tests/sysio.opreg_tests.cpp index e64e5e1315..bb76fd6141 100644 --- a/contracts/tests/sysio.opreg_tests.cpp +++ b/contracts/tests/sysio.opreg_tests.cpp @@ -1459,31 +1459,53 @@ BOOST_FIXTURE_TEST_CASE(flushwtdw_bounds_rows_per_epoch, sysio_opreg_tester) { t // WIRE-342 — registry ceilings bound the OPERATORS roster // --------------------------------------------------------------------------- -/// `epoch::MAX_ROSTER_OPERATORS` mirrored for the host tests: the epoch contract header -/// is CDT-only and cannot be included here, so the derivation is restated with its -/// inputs rather than as a bare number. If `sysio.epoch.hpp` changes any of these, this -/// constant diverges and the boundary case below fails — which is the intent: the -/// ceiling is consensus-visible and a silent change to it should not pass unnoticed. +/// `epoch::max_roster_operators` mirrored for the host tests: the epoch contract header +/// is CDT-only and cannot be included here, so the derivation is restated with its inputs +/// rather than as a bare number. If `sysio.epoch.hpp` changes any of these, this mirror +/// diverges and the boundary cases below fail — which is the intent: the ceiling is +/// consensus-visible and a silent change to it should not pass unnoticed. /// -/// (32768 envelope - 512 baseline - 24 overhead) / 2 share / 102 bytes-per-operator -static constexpr uint32_t EXPECTED_MAX_ROSTER_OPERATORS = (32768 - 512 - 24) / 2 / 102; +/// Two independent bounds, whichever is smaller: +/// bytes — (32768 envelope - 512 baseline - 24 overhead) / 2 share / bytes-per-operator +/// entries — the strictest outpost's roster capacity (Solana's `MAX_OPERATORS`) +static constexpr uint32_t EXPECTED_MAX_OUTPOST_ROSTER_ENTRIES = 128; + +static constexpr uint32_t expected_max_roster_operators(uint32_t outposts) { + const uint32_t addresses = outposts == 0 ? 1 : (outposts > 8 ? 8 : outposts); + const uint32_t bytes_per = 17 + addresses * 39 + 2 + 3 + 2; + const uint32_t by_bytes = (32768 - 512 - 24) / 2 / bytes_per; + return by_bytes < EXPECTED_MAX_OUTPOST_ROSTER_ENTRIES + ? by_bytes : EXPECTED_MAX_OUTPOST_ROSTER_ENTRIES; +} + +/// This fixture registers no chains, so the ceiling is the outpost entry cap. +static constexpr uint32_t EXPECTED_ROSTER_CEILING = expected_max_roster_operators(0); BOOST_FIXTURE_TEST_CASE(setconfig_accepts_shipped_default_ceilings, sysio_opreg_tester) { try { // The defaults must remain a legal configuration — a ceiling that rejected them would // brick `setconfig` for every existing cluster. BOOST_REQUIRE_EQUAL(success(), setconfig(21, 63, 21)); - BOOST_REQUIRE_LT(21u + 63u + 21u, EXPECTED_MAX_ROSTER_OPERATORS); + BOOST_REQUIRE_LT(21u + 63u + 21u, EXPECTED_ROSTER_CEILING); + + // The launch outpost set is {ETH, SOL}. At that shape the OUTPOST entry cap binds + // before the envelope does — so the depot cannot accept a configuration Solana would + // refuse to seat. Adding outposts fattens every entry and hands the bound back to + // bytes, which is the case a fixed bytes-per-operator constant used to get wrong. + BOOST_REQUIRE_EQUAL(EXPECTED_MAX_OUTPOST_ROSTER_ENTRIES, expected_max_roster_operators(2)); + BOOST_REQUIRE_LT(expected_max_roster_operators(4), expected_max_roster_operators(2)); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(setconfig_rejects_ceilings_exceeding_roster_capacity, sysio_opreg_tester) { try { // Exactly at the ceiling is legal; one over is not. Pinning both sides is what makes // this a boundary test rather than a smoke test. - const uint32_t at_ceiling = EXPECTED_MAX_ROSTER_OPERATORS; + const uint32_t at_ceiling = EXPECTED_ROSTER_CEILING; BOOST_REQUIRE_EQUAL(success(), setconfig(at_ceiling - 2, 1, 1)); BOOST_REQUIRE_EQUAL( error("assertion failure with message: max_available_* sum exceeds the OPERATORS " - "roster ceiling: the resulting roster could not fit an outbound envelope"), + "roster ceiling (" + std::to_string(EXPECTED_ROSTER_CEILING) + "): the " + "resulting roster could not fit an outbound envelope, or could not be seated " + "by an outpost"), setconfig(at_ceiling - 1, 1, 1)); // A governance-scale raise is the case this guard exists for: without it the roster @@ -1492,7 +1514,9 @@ BOOST_FIXTURE_TEST_CASE(setconfig_rejects_ceilings_exceeding_roster_capacity, sy // to change it through. BOOST_REQUIRE_EQUAL( error("assertion failure with message: max_available_* sum exceeds the OPERATORS " - "roster ceiling: the resulting roster could not fit an outbound envelope"), + "roster ceiling (" + std::to_string(EXPECTED_ROSTER_CEILING) + "): the " + "resulting roster could not fit an outbound envelope, or could not be seated " + "by an outpost"), setconfig(500, 500, 500)); } FC_LOG_AND_RETHROW() } @@ -1528,6 +1552,54 @@ BOOST_FIXTURE_TEST_CASE(regoperator_ceiling_bypassed_by_bootstrapped, sysio_opre regoperator("batchop.b"_n, OperatorType::OPERATOR_TYPE_BATCH, true)); } FC_LOG_AND_RETHROW() } +BOOST_FIXTURE_TEST_CASE(regoperator_global_roster_bound_applies_to_bootstrapped, + sysio_opreg_tester) { try { + // The PER-TYPE ceilings are a configured sum the bootstrap path bypasses, so on their + // own they are not a safety invariant. The GLOBAL bound is, and it must hold for + // bootstrapped registrations too: a bootstrapped operator lands ACTIVE immediately and + // is therefore in the roster at once. Bootstrap's collateral and termination + // exemptions are economic; exceeding a transport/seating ceiling is not a cost to the + // operator, it wedges the outpost for everyone. + BOOST_REQUIRE_EQUAL(success(), setconfig(21, 63, 21)); + + std::vector ops; + ops.reserve(EXPECTED_ROSTER_CEILING + 1); + for (uint32_t i = 0; i <= EXPECTED_ROSTER_CEILING; ++i) { + std::string account = "rostr"; + account += static_cast('a' + i / 26); + account += static_cast('a' + i % 26); + ops.emplace_back(account); + } + // Chunked: creating this many accounts in one block trips `block_cpu_usage_exceeded` + // before the test reaches what it is actually asserting. + for (size_t start = 0; start < ops.size(); start += 8) { + const size_t stop = std::min(start + 8, ops.size()); + create_accounts(std::vector(ops.begin() + start, ops.begin() + stop)); + produce_blocks(); + } + + // Fill the roster to exactly the ceiling, all bootstrapped so each lands ACTIVE and + // enters the roster immediately. Blocks are produced along the way: this many + // registrations in one block would exhaust the per-block billable CPU. + // One block per registration. Each `regoperator` now also walks the roster (bounded by + // the ceiling) on top of the per-type count, so batching even a handful into one block + // trips `block_cpu_usage_exceeded` well before the ceiling is reached. + for (uint32_t i = 0; i < EXPECTED_ROSTER_CEILING; ++i) { + BOOST_REQUIRE_EQUAL(success(), + regoperator(ops[i], OperatorType::OPERATOR_TYPE_BATCH, true)); + produce_blocks(); + } + + // The bypass tested above covers the PER-TYPE ceiling only. It must NOT extend to the + // global bound — this is the assertion that makes the sum a real invariant. + BOOST_REQUIRE_EQUAL( + error("assertion failure with message: OPERATORS roster ceiling reached (" + + std::to_string(EXPECTED_ROSTER_CEILING) + "): registering another operator " + "would produce a roster that cannot fit an outbound envelope or be seated by " + "an outpost"), + regoperator(ops[EXPECTED_ROSTER_CEILING], OperatorType::OPERATOR_TYPE_BATCH, true)); +} FC_LOG_AND_RETHROW() } + BOOST_FIXTURE_TEST_CASE(regoperator_ceiling_ignores_terminated_rows, sysio_opreg_tester) { try { // The count deliberately excludes TERMINATED rows. Counting them would let settled // history permanently exhaust the registry: terminated rows are erased only by the