Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
42 changes: 32 additions & 10 deletions contracts/sysio.msgch/src/sysio.msgch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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 @@ -79,6 +80,19 @@ 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;

/// 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 @@ -125,8 +139,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 @@ -799,8 +811,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 @@ -925,12 +937,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 @@ -1694,15 +1704,27 @@ void msgch::buildenv(uint64_t chain_code) {
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.

it = status_idx.erase(std::move(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
8 changes: 0 additions & 8 deletions contracts/sysio.uwrit/sysio.uwrit.abi
Original file line number Diff line number Diff line change
Expand Up @@ -588,14 +588,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
41 changes: 35 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 @@ -140,18 +168,19 @@ std::vector<char> encode_envelope_with_attestations(
constexpr size_t MAX_ENVELOPE_BYTES = 65'536;

/// 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 @@ -889,9 +918,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,
/*raw_att_type=*/3001,
std::string{});

BOOST_REQUIRE_EQUAL(success(), deliver(/*chain_code=*/eth_code, envelope));
Expand Down Expand Up @@ -1228,7 +1257,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
27 changes: 27 additions & 0 deletions contracts/tests/sysio.msgch_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,8 @@ constexpr uint64_t SOL_OUTPOST_ID = "SOL"_s.value;
constexpr auto EVM_TEST_ATTESTATION_TYPE = opp::types::ATTESTATION_TYPE_OPERATORS;
constexpr auto SWAP_REMIT_ATTESTATION_TYPE = opp::types::ATTESTATION_TYPE_SWAP_REMIT;
constexpr auto UNCOVERED_TEST_ATTESTATION_TYPE = opp::types::ATTESTATION_TYPE_STAKING_REWARD;
/// Raw protobuf wire slot used only to seed the pre-upgrade READY-row shape.
constexpr uint32_t RETIRED_STAKE_ATTESTATION_VALUE = 3001;

/// Decode the emitted OPP envelope and count attestations in its single message.
uint32_t emitted_attestation_count(const fc::variant& emitted_row) {
Expand Down Expand Up @@ -404,6 +406,31 @@ BOOST_FIXTURE_TEST_CASE(buildenv_writes_envlog_row, sysio_msgch_envlog_tester) {
BOOST_REQUIRE_EQUAL(31337u, row["endpoints"]["end"]["id"]["value"].as_uint64());
} FC_LOG_AND_RETHROW() }

/// READY rows written by a pre-upgrade contract with a retired staking wire
/// value are tombstoned before destination-specific envelope construction.
/// Active rows behind the tombstone still emit normally, so one legacy row
/// cannot strand the queue or reach an outpost decoder.
BOOST_FIXTURE_TEST_CASE(buildenv_tombstones_retired_staking_rows,

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.

Coverage gap: this only exercises the EVM path.

The scenario the tombstone primarily protects against is the SVM one — where an untombstoned retired row hits check(estimate.has_value(), "no Solana terminal account estimate for READY attestation") and hard-reverts buildenv. That path is untested.

Also untested: the retired-row-is-the-last-READY-row case, where erase must return the end iterator and the loop condition has to short-circuit before dereferencing.

Both are cheap additions to this same fixture.

sysio_msgch_envlog_tester) { try {
bootstrap_epoch_config(/*retention=*/200);
register_outpost(opp::types::CHAIN_KIND_EVM, 31337);
produce_blocks();

BOOST_REQUIRE_EQUAL(success(),
queueout(/*chain_code=*/ETH_OUTPOST_ID, RETIRED_STAKE_ATTESTATION_VALUE));
BOOST_REQUIRE_EQUAL(success(),
queueout(/*chain_code=*/ETH_OUTPOST_ID, EVM_TEST_ATTESTATION_TYPE));
BOOST_REQUIRE_EQUAL(2u, count_ready_attestations(ETH_OUTPOST_ID, 8));

BOOST_REQUIRE_EQUAL(success(), buildenv(/*chain_code=*/ETH_OUTPOST_ID));
produce_blocks();

BOOST_REQUIRE_EQUAL(0u, count_ready_attestations(ETH_OUTPOST_ID, 8));
const auto emitted = find_outbound_envelope();
BOOST_REQUIRE(!emitted.is_null());
BOOST_REQUIRE_EQUAL(1u, emitted_attestation_count(emitted));
} FC_LOG_AND_RETHROW() }

/// Eviction at the boundary. Set `retention=2` and one outpost →
/// `cap = 1*2*2 = 4`. After 5 buildenv rounds (5 rows inserted), the
/// oldest full epoch (`per_epoch = 1*2 = 2` rows) gets evicted; final
Expand Down
14 changes: 0 additions & 14 deletions etc/schema/opp_entity_diagram-gen.puml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@ sysio.opp.MessagePayload -- sysio.opp.AttestationEntry



sysio.opp.attestations.PretokenStakeChange -- sysio.opp.types.ChainAddress
sysio.opp.attestations.PretokenStakeChange -- sysio.opp.types.TokenAmount



sysio.opp.types.ChainSignature -- sysio.opp.types.ChainAddress
sysio.opp.types.ChainSignature -- sysio.opp.types.ChainKeyType

Expand Down Expand Up @@ -109,13 +104,6 @@ package sysio.opp.attestations {
index_at_mint: Long
}

class PretokenStakeChange {
actor: sysio.opp.types.ChainAddress
amount: sysio.opp.types.TokenAmount
index_at_mint: Long
index_at_burn: Long
}

class PretokenYield {
actor: sysio.opp.types.ChainAddress
amount: sysio.opp.types.TokenAmount
Expand Down Expand Up @@ -156,8 +144,6 @@ package sysio.opp.attestations {
package sysio.opp.types {
enum AttestationType {
ATTESTATION_TYPE_UNSPECIFIED
ATTESTATION_TYPE_STAKE
ATTESTATION_TYPE_UNSTAKE
ATTESTATION_TYPE_PRETOKEN_PURCHASE
ATTESTATION_TYPE_PRETOKEN_YIELD
ATTESTATION_TYPE_RESERVE_BALANCE_SHEET
Expand Down
2 changes: 1 addition & 1 deletion etc/schema/opp_entity_diagram-gen.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 0 additions & 19 deletions etc/schema/opp_entity_diagram.puml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ package opp.types #F5F5F5 {

enum attestation_type_t <<uint16>> {
reserve_balance_sheet = 0xAA00
stake = 0x0BB9
unstake = 0x0BBA
pretoken_purchase = 0x0BBB
pretoken_yield = 0x0BBE
--
Expand Down Expand Up @@ -149,21 +147,6 @@ package opp.attestations #FCE4EC {
* amounts : token_amount[]
}

entity stake {
* actor : chain_address
* amount : token_amount
* pretoken_count : uint256
* index_at_mint : uint256
}

entity unstake {
* unstaker : chain_address
* amount : token_amount
* pretoken_count : uint256
* index_at_burn : uint256

}

entity pretoken_purchase {
* actor : chain_address
* amount : token_amount
Expand Down Expand Up @@ -249,8 +232,6 @@ package opp.attestations #FCE4EC {

' -- Assertion entry resolves to concrete payloads --
opp.attestation_entry ..> opp.attestations.reserve_balance_sheet
opp.attestation_entry ..> opp.attestations.stake
opp.attestation_entry ..> opp.attestations.unstake
opp.attestation_entry ..> opp.attestations.pretoken_purchase
opp.attestation_entry ..> opp.attestations.pretoken_yield
opp.attestation_entry ..> opp.attestations.stake_update
Expand Down
2 changes: 0 additions & 2 deletions libraries/opp/include/sysio/opp/opp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ FC_REFLECT_ENUM(sysio::opp::types::ReserveStatus,
FC_REFLECT_ENUM(sysio::opp::types::AttestationType,
(ATTESTATION_TYPE_UNSPECIFIED)
(ATTESTATION_TYPE_OPERATOR_ACTION)
(ATTESTATION_TYPE_STAKE)
(ATTESTATION_TYPE_UNSTAKE)
(ATTESTATION_TYPE_PRETOKEN_PURCHASE)
(ATTESTATION_TYPE_PRETOKEN_YIELD)
(ATTESTATION_TYPE_RESERVE_BALANCE_SHEET)
Expand Down
13 changes: 4 additions & 9 deletions libraries/opp/proto/sysio/opp/attestations/attestations.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,13 @@ message ReserveBalanceSheet {
repeated sysio.opp.types.ReserveAmount reserves = 4;
}

// PretokenStakeChange was removed with the pre-launch STAKE / UNSTAKE
// lifecycle. Attestation enum slots 3001 and 3002 remain retired.

// ---------------------------------------------------------------------------
// Pre-launch specific attestations (DEPRECATED — kept for proto compatibility
// during the deprecation pass)
// Remaining pre-launch specific attestations (DEPRECATED)
// ---------------------------------------------------------------------------

message PretokenStakeChange {
sysio.opp.types.ChainAddress actor = 1;
sysio.opp.types.TokenAmount amount = 2;
int64 index_at_mint = 10;
int64 index_at_burn = 11;
}

message PretokenPurchase {
sysio.opp.types.ChainAddress actor = 1;
sysio.opp.types.TokenAmount amount = 2;
Expand Down
Loading
Loading