Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,9 @@ cliff.toml

# oh-my-claudecode runtime state (operational artifacts, never committed)
.omc/

# Build residue from containerised builds: the OPP bundle generator's pnpm
# store, and the throwaway HOME the build container needs because it runs as
# a non-root uid with no home of its own.
.pnpm-store/
.container-home/
119 changes: 0 additions & 119 deletions contracts/sysio.msgch/include/sysio.msgch/solana_terminal_budget.hpp

This file was deleted.

136 changes: 35 additions & 101 deletions contracts/sysio.msgch/src/sysio.msgch.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#include <sysio.msgch/sysio.msgch.hpp>
#include <sysio.msgch/solana_terminal_budget.hpp>
#include <sysio.epoch/sysio.epoch.hpp>
#include <sysio.authex/sysio.authex.hpp>
#include <sysio.chains/sysio.chains.hpp>
Expand Down Expand Up @@ -81,73 +80,6 @@ constexpr size_t ATTESTATION_OVERHEAD_BYTES = 24;
/// + payload preamble, and a safety margin for `zpp::bits` length prefixes.
constexpr size_t ENVELOPE_BASELINE_BYTES = 512;

using namespace sysio::msgch_svm_terminal_budget;

static_assert(svm_hard_dynamic_account_budget() == 16,
"SEC-94 SVM dynamic-account budget changed; update tests and terminal budget docs");

/// Pessimistic dynamic-account estimate for one Solana-bound outbound
/// attestation. `std::nullopt` means the attestation is not covered by the
/// SEC-94 manifest and must not be committed to a Solana envelope.
std::optional<size_t> estimate_svm_dynamic_accounts(AttestationType type,
const std::vector<char>& data) {
using AT = AttestationType;
switch (type) {
case AT::ATTESTATION_TYPE_OPERATORS:
case AT::ATTESTATION_TYPE_BATCH_OPERATOR_GROUPS:
case AT::ATTESTATION_TYPE_EMISSIONS_BLOCKED:
return SVM_DYNAMIC_ACCOUNTS_NONE;

case AT::ATTESTATION_TYPE_OPERATOR_ACTION: {
opp::attestations::OperatorAction oa;
auto in = zpp::bits::in{std::span{data.data(), data.size()}, zpp::bits::no_size{}};
if (in(oa) != zpp::bits::errc{}) return std::nullopt;

using OAT = opp::attestations::OperatorAction;
switch (oa.action_type) {
case OAT::ACTION_TYPE_WITHDRAW_REMIT:
return SVM_DYNAMIC_ACCOUNTS_NATIVE_WALLET;
case OAT::ACTION_TYPE_SLASH:
return SVM_DYNAMIC_ACCOUNTS_NONE;
case OAT::ACTION_TYPE_DEPOSIT_REQUEST:
case OAT::ACTION_TYPE_WITHDRAW_REQUEST:
case OAT::ACTION_TYPE_UNKNOWN:
default:
return std::nullopt;
}
}

case AT::ATTESTATION_TYPE_DEPOSIT_REVERT:
case AT::ATTESTATION_TYPE_RESERVE_READY:
return SVM_DYNAMIC_ACCOUNTS_NATIVE_WALLET;

case AT::ATTESTATION_TYPE_SWAP_REMIT:
case AT::ATTESTATION_TYPE_SWAP_REVERT:
case AT::ATTESTATION_TYPE_RESERVE_CREATE_CANCELLED:
return SVM_DYNAMIC_ACCOUNTS_RESERVE_EFFECT_WORST_CASE;

case AT::ATTESTATION_TYPE_UNSPECIFIED:
case AT::ATTESTATION_TYPE_STAKE:
case AT::ATTESTATION_TYPE_UNSTAKE:
case AT::ATTESTATION_TYPE_PRETOKEN_PURCHASE:
case AT::ATTESTATION_TYPE_PRETOKEN_YIELD:
case AT::ATTESTATION_TYPE_WIRE_TOKEN_PURCHASE:
case AT::ATTESTATION_TYPE_RESERVE_BALANCE_SHEET:
case AT::ATTESTATION_TYPE_STAKE_UPDATE:
case AT::ATTESTATION_TYPE_CHALLENGE_RESPONSE:
case AT::ATTESTATION_TYPE_SWAP_REQUEST:
case AT::ATTESTATION_TYPE_CHALLENGE_REQUEST:
case AT::ATTESTATION_TYPE_NODE_OWNER_REG:
case AT::ATTESTATION_TYPE_STAKING_REWARD:
case AT::ATTESTATION_TYPE_STAKE_RESULT:
case AT::ATTESTATION_TYPE_ATTESTATION_PROCESSING_ERROR:
case AT::ATTESTATION_TYPE_UNDERWRITE_INTENT_COMMIT:
case AT::ATTESTATION_TYPE_RESERVE_CREATE:
case AT::ATTESTATION_TYPE_RESERVE_CREATE_CANCEL:
default:
return std::nullopt;
}
}

uint32_t current_epoch_index() {
epoch::epochstate_t tbl(EPOCH_ACCOUNT);
Expand Down Expand Up @@ -1070,7 +1002,8 @@ void dispatch_attestation(name self, uint64_t attestation_id,
// `inbound_envelope_valid`, which `deliver` already ran at ingress and which is re-checked below.
const auto op_row = [&]() {
sysio::chains::chains_t chains_tbl(CHAINS_ACCOUNT);
return chains_tbl.get(sysio::chains::chain_key{sysio::slug_name{chain_code}});
return chains_tbl.get(sysio::chains::chain_key{sysio::slug_name{chain_code}},
"sysio.msgch: inbound envelope names an unregistered chain_code");
}();
// Validate the envelope-level chain + per-message semantic headers -- the SAME check `deliver`
// runs at ingress (see `inbound_envelope_valid`), re-run here as defense in depth. A drop is a
Expand Down Expand Up @@ -1636,15 +1569,25 @@ void msgch::queueout(uint64_t chain_code,
has_auth(RESERV_ACCOUNT) || has_auth(get_self()),
"queueout: caller not authorized to queue outbound attestations");

{
sysio::chains::chains_t chains_tbl(CHAINS_ACCOUNT);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queueout lost its only chain_code registration check — an authorized caller's typo now creates a permanent, unreachable READY row.

All 8 production call sites derive their code from sysio.chains (epoch:686,729; opreg:634,673,745; uwrit:349,561; reserv:153), so this is not an attack vector — but it is an ops footgun: a governance msig holding one of the authorized authorities pushing queueout directly with an unregistered code used to abort with "chain_code is not registered"; now it succeeds silently. epoch::advance only fans buildenv to registered active outposts, the PROCESSED-drain (lines 1853-1860) skips mismatched chain codes, and no prune action exists — the row is permanent sysio-pool RAM, re-walked by the Phase-1 bystatus scan on every buildenv for every outpost, every epoch.

Related cleanup if the removal stands: sysio.reserv.cpp:93-95 ("sysio.msgch::queueout fails loudly for direct callers"), sysio.msgch_tests.cpp:164-170, and sysio.reserv_tests.cpp:411-414 all still describe the deleted guard — a future reader will conclude msgch is the backstop and relax the reserv-side guards. Also buildenv's chains fetch (new lines 1648-1651) now asserts with the bare default "key not found" — worth passing the message overload.

auto chain = chains_tbl.get(sysio::chains::chain_key{sysio::slug_name{chain_code}},
"sysio.msgch::queueout: chain_code is not registered");
if (chain.kind == ChainKind::CHAIN_KIND_SVM) {
check(estimate_svm_dynamic_accounts(attest_type, data).has_value(),
"sysio.msgch::queueout: no Solana terminal account estimate for attestation");
}
}
// The chains registry is the ONLY authority on which chain codes exist.
//
// This is not an attack gate -- the authorization check above is. It is an
// ops gate: all eight production call sites derive their code from
// sysio.chains, but a governance msig holding one of those authorities can
// call queueout directly, and a typo'd code used to abort here. Without it
// the row is created READY and is then unreachable forever: epoch::advance
// only fans buildenv to registered active outposts, the PROCESSED drain
// skips mismatched codes, and no prune action exists -- so it is permanent
// sysio-pool RAM, re-walked by the Phase-1 bystatus scan on every buildenv,
// for every outpost, every epoch.
//
// The SVM terminal-account estimator that used to live beside this lookup
// was deliberately deleted (it made the depot model another chain's packet
// limit); the registration assert was scaffolding for it and fell out by
// accident. Only the assert comes back.
sysio::chains::chains_t chains_tbl(CHAINS_ACCOUNT);
chains_tbl.get(sysio::chains::chain_key{sysio::slug_name{chain_code}},
"sysio.msgch::queueout: chain_code is not registered");

auto now_sec = static_cast<uint64_t>(current_time_point().sec_since_epoch());

Expand Down Expand Up @@ -1725,9 +1668,9 @@ void msgch::buildenv(uint64_t chain_code) {

const auto op_row = [&]() {
sysio::chains::chains_t chains_tbl(CHAINS_ACCOUNT);
return chains_tbl.get(sysio::chains::chain_key{sysio::slug_name{chain_code}});
return chains_tbl.get(sysio::chains::chain_key{sysio::slug_name{chain_code}},
"sysio.msgch::buildenv: chain_code is not registered");
}();
const bool is_svm_destination = op_row.kind == ChainKind::CHAIN_KIND_SVM;

// Route endpoints, stamped into the wire envelope below and mirrored on the envlog audit
// row (symmetric with the evalcons inbound projection: `kind` → ChainId.kind,
Expand All @@ -1743,40 +1686,31 @@ void msgch::buildenv(uint64_t chain_code) {

// Phase 2: estimator-based initial pick. Walk candidates in order, accumulating a conservative byte
// estimate; stop once the next one would push the envelope over MAX_ENVELOPE_BYTES. The trim loop
// below is the source of truth for the encoded size invariant; for SVM destinations this pass also
// enforces the consensus-critical terminal transaction budget before the envelope is committed.
// below is the source of truth for the encoded size invariant.
//
// This pass deliberately does NOT bound the envelope by any destination chain's transaction
// capacity. Solana dispatch is resumable: the outpost settles `[cursor, cursor + dispatch_limit)`
// per terminal call and the relay loops until the cursor drains, so an envelope that needs more
// effect accounts than one Solana transaction can carry is settled across several calls rather
// than refused here. Sizing a WIRE consensus envelope against another chain's packet limit put
// that chain's MTU inside depot consensus; the cursor removes the need.
size_t included_count = 0;
size_t estimated_bytes = ENVELOPE_BASELINE_BYTES;
size_t estimated_svm_dynamic_accounts = 0;
for (const auto& entry : candidate_entries) {
const size_t entry_bytes = ATTESTATION_OVERHEAD_BYTES + entry.data.size();
if (estimated_bytes + entry_bytes > MAX_ENVELOPE_BYTES) {
break;
}
size_t next_svm_dynamic_accounts = estimated_svm_dynamic_accounts;
size_t entry_svm_dynamic_accounts = 0;
if (is_svm_destination) {
auto estimate = estimate_svm_dynamic_accounts(entry.type, entry.data);
check(estimate.has_value(),
"sysio.msgch::buildenv: no Solana terminal account estimate for READY attestation");
entry_svm_dynamic_accounts = *estimate;
next_svm_dynamic_accounts += entry_svm_dynamic_accounts;
if (!svm_terminal_budget_fits(next_svm_dynamic_accounts)) {
break;
}
}
estimated_bytes += entry_bytes;
estimated_svm_dynamic_accounts = next_svm_dynamic_accounts;
++included_count;
}

// First-attestation-too-big guard. The estimator picks zero only when the first candidate alone
// overshoots the envelope or SVM terminal cap; the trim loop below would surface the same envelope
// condition, but aborting upfront avoids building anything in the doomed case. Never expected at
// protocol level because every valid current Solana-bound attestation should fit by itself.
// 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 or Solana terminal budget");
"sysio.msgch::buildenv: a single READY attestation exceeds the outbound envelope");

std::vector<opp::AttestationEntry> entries(
std::make_move_iterator(candidate_entries.begin()),
Expand Down
Binary file modified contracts/sysio.msgch/sysio.msgch.wasm
Binary file not shown.
Loading