Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
Binary file modified contracts/sysio.authex/sysio.authex.wasm
Binary file not shown.
Binary file modified contracts/sysio.chalg/sysio.chalg.wasm
Binary file not shown.
52 changes: 42 additions & 10 deletions contracts/sysio.msgch/src/sysio.msgch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <sysio/opp/attestations/attestations.pb.hpp>
#include <zpp_bits.h>
#include <algorithm>
#include <magic_enum/magic_enum.hpp>
#include <optional>

namespace sysio {
Expand Down Expand Up @@ -81,6 +82,23 @@ 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;

/// Retired pre-launch attestation wire slots. They remain recognizable here
/// only so an upgraded contract can tombstone READY rows queued by the prior
/// implementation instead of forwarding them or blocking envelope creation.
constexpr int32_t RETIRED_STAKE_ATTESTATION_VALUE = 3001;
constexpr int32_t RETIRED_UNSTAKE_ATTESTATION_VALUE = 3002;

/// Bound legacy-row cleanup work performed by one buildenv action. Remaining
/// tombstones stay READY for a later call but are never envelope candidates.
constexpr size_t MAX_RETIRED_STAKING_PRUNE_PER_BUILD = 32;

/// Return true when an attestation carries a retired pre-launch staking slot.
bool is_retired_staking_attestation(AttestationType type) {
const auto value = magic_enum::enum_integer(type);
return value == RETIRED_STAKE_ATTESTATION_VALUE ||
value == RETIRED_UNSTAKE_ATTESTATION_VALUE;
}

using namespace sysio::msgch_svm_terminal_budget;

static_assert(svm_hard_dynamic_account_budget() == 16,
Expand Down Expand Up @@ -127,8 +145,6 @@ std::optional<size_t> estimate_svm_dynamic_accounts(AttestationType type,
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:
Expand Down Expand Up @@ -801,8 +817,8 @@ void dispatch_node_owner_reg(const std::vector<char>& data, uint64_t chain_code)
/// in `evalcons` after a consensus envelope has been unpacked. Dispatch is
/// best-effort — silently no-ops on unknown / out-of-scope types so the
/// inbound stream can keep flowing even when the depot hasn't yet wired up
/// every handler (e.g. the deferred STAKE / UNSTAKE / STAKE_UPDATE staking
/// lifecycle types).
/// every active handler (for example, STAKE_UPDATE from the separate staking
/// track). Retired STAKE / UNSTAKE wire values also land on this no-op path.
void dispatch_attestation(name self, uint64_t attestation_id,
AttestationType type,
const std::vector<char>& data,
Expand Down Expand Up @@ -927,12 +943,10 @@ void dispatch_attestation(name self, uint64_t attestation_id,
// opening a sysio.chalg dispute vote, not by inbound challenge attestations.
break;

case AttestationType::ATTESTATION_TYPE_STAKE:
case AttestationType::ATTESTATION_TYPE_UNSTAKE:
case AttestationType::ATTESTATION_TYPE_STAKE_UPDATE:
case AttestationType::ATTESTATION_TYPE_STAKE_RESULT:
// Validator-staking lifecycle; depot-side handlers land in a later
// task alongside liqEth / liqsol-token wiring.
// Post-launch validator-staking lifecycle; depot-side handlers land
// alongside liqEth / liqsol-token wiring.
break;

// Outbound-only types (depot emits these, never receives them inbound)
Expand Down Expand Up @@ -1707,18 +1721,36 @@ void msgch::buildenv(uint64_t chain_code) {
std::vector<uint64_t> candidate_ids;

auto status_idx = atts.get_index<"bystatus"_n>();
size_t retired_staking_pruned = 0;
for (auto it = status_idx.lower_bound(
static_cast<uint64_t>(AttestationStatus::ATTESTATION_STATUS_READY));
it != status_idx.end() &&
it->status == AttestationStatus::ATTESTATION_STATUS_READY; ++it) {
if (it->chain_code != chain_code) continue;
it->status == AttestationStatus::ATTESTATION_STATUS_READY; ) {
// Upgrade tombstone: legacy builds could persist STAKE / UNSTAKE rows.
// Erase them before destination-specific estimation so neither the SVM
// terminal-account gate nor an outpost decoder can be blocked by a
// protocol value that no longer has a generated enum/message type.
if (is_retired_staking_attestation(it->type)) {

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.

Unbounded prune inside an inline action.

This erases every retired READY row across all chains in a single transaction, and buildenv runs as an inline action inside sysio.epoch::advance.

If the residual legacy row count is large enough that N × (primary erase + 3 secondary-index erases) exceeds the transaction CPU budget, advance reverts. The erases roll back with it, so every subsequent advance retries the identical work and reverts again — a deterministic, non-self-healing epoch stall.

The immediately preceding master commit (3673936b9f, uwrit UWREQ expiry) used bounded epoch-driven pruning for this same class of migration. A per-call cap plus a "skip, don't candidate" branch for the over-budget remainder would make this drain across epochs instead of all-or-nothing.

Rating this low because the realistic residual count is small — an EVM-bound STAKE row would already have been packed and drained by the pre-upgrade contract, so only SVM-bound rows survive — but the failure mode has no recovery path if it does land.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed on current head 2c29ce1. buildenv caps retired-row erasure at 32 per call and continues skipping every over-budget retired row as an envelope candidate, so cleanup drains deterministically across calls without forwarding tombstones or turning one advance into an unbounded erase sweep. Coverage includes 33 retired rows followed by an active row; the active row emits and exactly one tombstone remains READY.

if (retired_staking_pruned < MAX_RETIRED_STAKING_PRUNE_PER_BUILD) {
it = status_idx.erase(std::move(it));
++retired_staking_pruned;
} else {
++it;
}
continue;
}
if (it->chain_code != chain_code) {
++it;
continue;
}

opp::AttestationEntry entry;
entry.type = it->type;
entry.data_size = zpp::bits::vuint32_t{static_cast<uint32_t>(it->data.size())};
entry.data = it->data;
candidate_entries.push_back(std::move(entry));
candidate_ids.push_back(it->id);
++it;
}

if (candidate_entries.empty()) return;
Expand Down
8 changes: 0 additions & 8 deletions contracts/sysio.msgch/sysio.msgch.abi
Original file line number Diff line number Diff line change
Expand Up @@ -582,14 +582,6 @@
"name": "ATTESTATION_TYPE_OPERATOR_ACTION",
"value": 2001
},
{
"name": "ATTESTATION_TYPE_STAKE",
"value": 3001
},
{
"name": "ATTESTATION_TYPE_UNSTAKE",
"value": 3002
},
{
"name": "ATTESTATION_TYPE_PRETOKEN_PURCHASE",
"value": 3004
Expand Down
Binary file modified contracts/sysio.msgch/sysio.msgch.wasm
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,8 @@ DataStream& operator>>(DataStream& ds, ReserveBalanceSheet& t) {
return ds >> t.chain_code >> t.reserves;
}

// PretokenStakeChange (deprecated; pre-launch only)
template <typename DataStream>
DataStream& operator<<(DataStream& ds, const PretokenStakeChange& t) {
return ds << t.actor << t.amount << t.index_at_mint << t.index_at_burn;
}
template <typename DataStream>
DataStream& operator>>(DataStream& ds, PretokenStakeChange& t) {
return ds >> t.actor >> t.amount >> t.index_at_mint >> t.index_at_burn;
}
// PretokenStakeChange DataStream operators were removed with the retired
// pre-launch STAKE / UNSTAKE lifecycle (enum slots 3001 and 3002).

// PretokenPurchase (deprecated; pre-launch only)
template <typename DataStream>
Expand Down
Binary file modified contracts/sysio.reserv/sysio.reserv.wasm
Binary file not shown.
Binary file modified contracts/sysio.roa/sysio.roa.wasm
Binary file not shown.
8 changes: 0 additions & 8 deletions contracts/sysio.uwrit/sysio.uwrit.abi
Original file line number Diff line number Diff line change
Expand Up @@ -624,14 +624,6 @@
"name": "ATTESTATION_TYPE_OPERATOR_ACTION",
"value": 2001
},
{
"name": "ATTESTATION_TYPE_STAKE",
"value": 3001
},
{
"name": "ATTESTATION_TYPE_UNSTAKE",
"value": 3002
},
{
"name": "ATTESTATION_TYPE_PRETOKEN_PURCHASE",
"value": 3004
Expand Down
Binary file modified contracts/sysio.uwrit/sysio.uwrit.wasm
Binary file not shown.
44 changes: 38 additions & 6 deletions contracts/tests/sysio.dispatch_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,34 @@ std::vector<char> encode_envelope_with_one_attestation(
return out;
}

/// Encode a single historical attestation whose numeric enum slot is no
/// longer declared by the current protobuf schema.
std::vector<char> encode_envelope_with_one_raw_attestation(
uint32_t epoch_index,
int32_t raw_att_type,
const std::string& att_data)
{
sysio::opp::Envelope env;
env.set_epoch_index(epoch_index);
env.set_epoch_envelope_index(1);
env.set_epoch_timestamp(1'775'612'516'983ULL);

auto* msg = env.add_messages();
auto* payload = msg->mutable_payload();
auto* att = payload->add_attestations();
const auto* field = att->GetDescriptor()->FindFieldByName("type");
BOOST_REQUIRE(field != nullptr);
att->GetReflection()->SetEnumValue(att, field, raw_att_type);
att->set_data(att_data);
att->set_data_size(static_cast<uint32_t>(att_data.size()));

oracle::finalize_header(*env.mutable_messages(0), {}, 1'775'612'516'983ULL);

std::vector<char> out(env.ByteSizeLong());
env.SerializeToArray(out.data(), static_cast<int>(out.size()));
return out;
}

/// Encode an Envelope wrapping N attestations of the same type. Used to fit
/// multiple OPERATOR_ACTIONs into a single delivery, since the depot
/// deduplicates per-(batch_op, outpost, epoch) — a second `deliver` from
Expand Down Expand Up @@ -139,19 +167,23 @@ std::vector<char> encode_envelope_with_attestations(
/// mirror, same as the outbound packing tests in sysio.msgch_tests.cpp.
constexpr size_t MAX_ENVELOPE_BYTES = 65'536;

/// Historical STAKE protobuf wire value used to verify dispatch drops retired types.
constexpr int32_t RETIRED_STAKE_ATTESTATION_VALUE = 3001;

/// Encode a decodable envelope whose serialised size is EXACTLY `target_bytes`, padded with a
/// single out-of-scope STAKE attestation (dispatch drops it with no value-bearing effect). Probe
/// single challenge-response attestation (dispatch drops it with no value-bearing effect). Probe
/// once with `target_bytes` of padding to measure the fixed protobuf overhead, then rebuild with
/// the pad shrunk by that overhead: at sizes near the 64 KiB envelope cap every nested length
/// prefix and the `data_size` varint sit in the same 3-byte width band (16 KiB .. 2 MiB), so the
/// second pass lands exactly on target — the final REQUIRE pins it.
std::vector<char> encode_envelope_padded_to(uint32_t epoch_index, size_t target_bytes) {
auto probe = encode_envelope_with_one_attestation(
epoch_index, sysio::opp::types::ATTESTATION_TYPE_STAKE, std::string(target_bytes, 'x'));
epoch_index, sysio::opp::types::ATTESTATION_TYPE_CHALLENGE_RESPONSE,
std::string(target_bytes, 'x'));
BOOST_REQUIRE_GT(probe.size(), target_bytes);
const size_t overhead = probe.size() - target_bytes;
auto padded = encode_envelope_with_one_attestation(
epoch_index, sysio::opp::types::ATTESTATION_TYPE_STAKE,
epoch_index, sysio::opp::types::ATTESTATION_TYPE_CHALLENGE_RESPONSE,
std::string(target_bytes - overhead, 'x'));
BOOST_REQUIRE_EQUAL(target_bytes, padded.size());
return padded;
Expand Down Expand Up @@ -981,9 +1013,9 @@ BOOST_FIXTURE_TEST_CASE(dispatch_silently_drops_out_of_scope_types, sysio_dispat
bootstrap_for_dispatch();

const auto eth_code = fc::slug_name{"ETH"}.value;
auto envelope = encode_envelope_with_one_attestation(
auto envelope = encode_envelope_with_one_raw_attestation(
current_epoch(),
sysio::opp::types::ATTESTATION_TYPE_STAKE,
RETIRED_STAKE_ATTESTATION_VALUE,
std::string{});

BOOST_REQUIRE_EQUAL(success(), deliver(/*chain_code=*/eth_code, envelope));
Expand Down Expand Up @@ -1320,7 +1352,7 @@ BOOST_FIXTURE_TEST_CASE(deliver_duplicate_from_same_operator_reverts, sysio_disp
const auto eth_code = fc::slug_name{"ETH"}.value;
auto envelope = encode_envelope_with_one_attestation(
current_epoch(),
sysio::opp::types::ATTESTATION_TYPE_STAKE,
sysio::opp::types::ATTESTATION_TYPE_CHALLENGE_RESPONSE,
std::string{});

BOOST_REQUIRE_EQUAL(success(), deliver(/*chain_code=*/eth_code, envelope));
Expand Down
8 changes: 4 additions & 4 deletions contracts/tests/sysio.msgch_chain_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,9 @@ class sysio_msgch_chain_tester : public tester {

// -- Inbound envelope builder --

/// Encode a deliverable envelope carrying one out-of-scope STAKE attestation (dispatch drops
/// the attestation silently; acceptance is still fully observable via `outpcons` and the
/// stored attestation row). The semantic header is derived per the spec — `apply_consensus`
/// Encode a deliverable envelope carrying one out-of-scope CHALLENGE_RESPONSE attestation
/// (dispatch drops the attestation silently; acceptance is still fully observable via
/// `outpcons` and the stored attestation row). The semantic header is derived per the spec — `apply_consensus`
/// drops envelopes whose header fields do not recompute or whose message does not continue the
/// per-outpost message chain. `prev` (previous_envelope_hash), `prev_message_id`, and
/// `env_hash` are raw 32-byte strings (or empty for stream genesis).
Expand All @@ -375,7 +375,7 @@ class sysio_msgch_chain_tester : public tester {
if (!prev.empty()) env.set_previous_envelope_hash(prev);
if (!env_hash.empty()) env.set_envelope_hash(env_hash);
auto* att = env.add_messages()->mutable_payload()->add_attestations();
att->set_type(sysio::opp::types::ATTESTATION_TYPE_STAKE);
att->set_type(sysio::opp::types::ATTESTATION_TYPE_CHALLENGE_RESPONSE);
att->set_data(att_data);
att->set_data_size(static_cast<uint32_t>(att_data.size()));
oracle::finalize_header(*env.mutable_messages(0), prev_message_id, 1'775'612'516'983ULL);
Expand Down
Loading
Loading