From 10a8884fc92f086d1881f59f69480f7a26b6651c Mon Sep 17 00:00:00 2001 From: swampdaddy Date: Fri, 13 Mar 2026 17:11:31 +0000 Subject: [PATCH] depot contract v2: all 7 process_assertion handlers + deadlock fix - balance_sheet (0xAA00): deserialize + upsert reserves - stake_update (0xEE00): update reserves + queue outbound - yield_reward (0xEE01): calculate + inline transfer WIRE - wire_purchase (0xEE02): constant-product swap + transfer - operator_registration (0xEE03): create/reactivate/deregister - challenge_response (0xEE04): route to challenge FSM - slash_operator (0xEE05): slash + queue outbound ack - default: silently pass unknown types (fixes permanent deadlock) - includes compiled wasm (110,570 bytes) and ABI --- artifacts/sysio.depot.v2-handlers.abi | 1056 +++++++++++++++++ artifacts/sysio.depot.v2-handlers.wasm | Bin 0 -> 110570 bytes .../include/sysio.depot/depot.tables.hpp | 273 +++++ .../include/sysio.depot/depot.types.hpp | 106 ++ .../include/sysio.depot/sysio.depot.hpp | 166 +++ contracts/sysio.depot/src/challenge.cpp | 352 ++++++ contracts/sysio.depot/src/consensus.cpp | 146 +++ contracts/sysio.depot/src/operators.cpp | 248 ++++ contracts/sysio.depot/src/opp_inbound.cpp | 219 ++++ contracts/sysio.depot/src/opp_outbound.cpp | 120 ++ contracts/sysio.depot/src/reserves.cpp | 181 +++ contracts/sysio.depot/src/sysio.depot.cpp | 545 +++++++++ contracts/sysio.depot/src/underwriting.cpp | 150 +++ 13 files changed, 3562 insertions(+) create mode 100644 artifacts/sysio.depot.v2-handlers.abi create mode 100755 artifacts/sysio.depot.v2-handlers.wasm create mode 100644 contracts/sysio.depot/include/sysio.depot/depot.tables.hpp create mode 100644 contracts/sysio.depot/include/sysio.depot/depot.types.hpp create mode 100644 contracts/sysio.depot/include/sysio.depot/sysio.depot.hpp create mode 100644 contracts/sysio.depot/src/challenge.cpp create mode 100644 contracts/sysio.depot/src/consensus.cpp create mode 100644 contracts/sysio.depot/src/operators.cpp create mode 100644 contracts/sysio.depot/src/opp_inbound.cpp create mode 100644 contracts/sysio.depot/src/opp_outbound.cpp create mode 100644 contracts/sysio.depot/src/reserves.cpp create mode 100644 contracts/sysio.depot/src/sysio.depot.cpp create mode 100644 contracts/sysio.depot/src/underwriting.cpp diff --git a/artifacts/sysio.depot.v2-handlers.abi b/artifacts/sysio.depot.v2-handlers.abi new file mode 100644 index 0000000..a61f7c5 --- /dev/null +++ b/artifacts/sysio.depot.v2-handlers.abi @@ -0,0 +1,1056 @@ +{ + "____comment": "This file was generated with sysio-abigen. DO NOT EDIT ", + "version": "sysio::abi/1.2", + "types": [ + { + "new_type_name": "assertion_type_t", + "type": "uint16" + }, + { + "new_type_name": "chain_kind_t", + "type": "uint8" + }, + { + "new_type_name": "chain_status_t", + "type": "uint8" + }, + { + "new_type_name": "challenge_status_t", + "type": "uint8" + }, + { + "new_type_name": "depot_state_t", + "type": "uint8" + }, + { + "new_type_name": "message_direction_t", + "type": "uint8" + }, + { + "new_type_name": "message_status_t", + "type": "uint8" + }, + { + "new_type_name": "operator_status_t", + "type": "uint8" + }, + { + "new_type_name": "operator_type_t", + "type": "uint8" + }, + { + "new_type_name": "underwrite_status_t", + "type": "uint8" + } + ], + "structs": [ + { + "name": "activateop", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "name" + } + ] + }, + { + "name": "bootstrap", + "base": "", + "fields": [] + }, + { + "name": "challenge", + "base": "", + "fields": [ + { + "name": "challenger", + "type": "name" + }, + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "evidence", + "type": "bytes" + } + ] + }, + { + "name": "challenge_info", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "status", + "type": "challenge_status_t" + }, + { + "name": "round", + "type": "uint8" + }, + { + "name": "challenge_data", + "type": "bytes" + } + ] + }, + { + "name": "chalresolve", + "base": "", + "fields": [ + { + "name": "proposer", + "type": "name" + }, + { + "name": "challenge_id", + "type": "uint64" + }, + { + "name": "original_hash", + "type": "checksum256" + }, + { + "name": "round1_hash", + "type": "checksum256" + }, + { + "name": "round2_hash", + "type": "checksum256" + } + ] + }, + { + "name": "chalresp", + "base": "", + "fields": [ + { + "name": "operator_account", + "type": "name" + }, + { + "name": "challenge_id", + "type": "uint64" + }, + { + "name": "response_data", + "type": "bytes" + } + ] + }, + { + "name": "chalvote", + "base": "", + "fields": [ + { + "name": "voter", + "type": "name" + }, + { + "name": "challenge_id", + "type": "uint64" + }, + { + "name": "approve", + "type": "bool" + } + ] + }, + { + "name": "crank", + "base": "", + "fields": [ + { + "name": "operator_account", + "type": "name" + } + ] + }, + { + "name": "depot_global_state", + "base": "", + "fields": [ + { + "name": "state", + "type": "depot_state_t" + }, + { + "name": "chain_id", + "type": "chain_kind_t" + }, + { + "name": "current_epoch", + "type": "uint64" + }, + { + "name": "next_epoch", + "type": "uint64" + }, + { + "name": "next_msg_out", + "type": "uint64" + }, + { + "name": "last_crank_time", + "type": "time_point_sec" + }, + { + "name": "token_contract", + "type": "name" + }, + { + "name": "initialized", + "type": "bool" + } + ] + }, + { + "name": "emitchain", + "base": "", + "fields": [ + { + "name": "operator_account", + "type": "name" + }, + { + "name": "epoch_number", + "type": "uint64" + } + ] + }, + { + "name": "epoch_vote", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "operator_id", + "type": "uint64" + }, + { + "name": "chain_hash", + "type": "checksum256" + }, + { + "name": "submitted_at", + "type": "time_point_sec" + } + ] + }, + { + "name": "exitop", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "name" + } + ] + }, + { + "name": "getquote", + "base": "", + "fields": [ + { + "name": "source_sym", + "type": "symbol" + }, + { + "name": "target_sym", + "type": "symbol" + }, + { + "name": "amount", + "type": "asset" + } + ] + }, + { + "name": "init", + "base": "", + "fields": [ + { + "name": "chain_id", + "type": "chain_kind_t" + }, + { + "name": "token_contract", + "type": "name" + } + ] + }, + { + "name": "known_operator", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "op_type", + "type": "operator_type_t" + }, + { + "name": "status", + "type": "operator_status_t" + }, + { + "name": "wire_account", + "type": "name" + }, + { + "name": "secp256k1_pubkey", + "type": "bytes" + }, + { + "name": "ed25519_pubkey", + "type": "bytes" + }, + { + "name": "collateral", + "type": "asset" + }, + { + "name": "registered_at", + "type": "time_point_sec" + }, + { + "name": "status_changed_at", + "type": "time_point_sec" + } + ] + }, + { + "name": "message_chain", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "direction", + "type": "message_direction_t" + }, + { + "name": "status", + "type": "chain_status_t" + }, + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "merkle_root", + "type": "checksum256" + }, + { + "name": "epoch_hash", + "type": "checksum256" + }, + { + "name": "prev_epoch_hash", + "type": "checksum256" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "operator_signature", + "type": "bytes" + }, + { + "name": "operator_id", + "type": "uint64" + }, + { + "name": "created_at", + "type": "time_point_sec" + } + ] + }, + { + "name": "oneshot", + "base": "", + "fields": [ + { + "name": "beneficiary", + "type": "name" + }, + { + "name": "amount", + "type": "asset" + } + ] + }, + { + "name": "op_schedule", + "base": "", + "fields": [ + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "elected_operator_ids", + "type": "uint64[]" + }, + { + "name": "created_at", + "type": "time_point_sec" + } + ] + }, + { + "name": "opp_epoch_in", + "base": "", + "fields": [ + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "start_message", + "type": "uint64" + }, + { + "name": "end_message", + "type": "uint64" + }, + { + "name": "epoch_merkle", + "type": "checksum256" + }, + { + "name": "challenge_flag", + "type": "bool" + } + ] + }, + { + "name": "opp_epoch_out", + "base": "", + "fields": [ + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "start_message", + "type": "uint64" + }, + { + "name": "end_message", + "type": "uint64" + }, + { + "name": "merkle_root", + "type": "checksum256" + } + ] + }, + { + "name": "opp_fork", + "base": "", + "fields": [ + { + "name": "fork_id", + "type": "uint64" + }, + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "end_message_id", + "type": "uint64" + }, + { + "name": "merkle_root", + "type": "checksum256" + } + ] + }, + { + "name": "opp_fork_vote", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "fork_id", + "type": "uint64" + }, + { + "name": "voter", + "type": "name" + }, + { + "name": "vote_state", + "type": "uint8" + } + ] + }, + { + "name": "opp_message_in", + "base": "", + "fields": [ + { + "name": "message_number", + "type": "uint64" + }, + { + "name": "assertion_type", + "type": "assertion_type_t" + }, + { + "name": "status", + "type": "message_status_t" + }, + { + "name": "payload", + "type": "bytes" + } + ] + }, + { + "name": "opp_message_out", + "base": "", + "fields": [ + { + "name": "message_number", + "type": "uint64" + }, + { + "name": "assertion_type", + "type": "assertion_type_t" + }, + { + "name": "payload", + "type": "bytes" + } + ] + }, + { + "name": "regoperator", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "name" + }, + { + "name": "op_type", + "type": "operator_type_t" + }, + { + "name": "secp256k1_pubkey", + "type": "bytes" + }, + { + "name": "ed25519_pubkey", + "type": "bytes" + }, + { + "name": "collateral", + "type": "asset" + } + ] + }, + { + "name": "reserve_balance", + "base": "", + "fields": [ + { + "name": "reserve_total", + "type": "asset" + }, + { + "name": "wire_equivalent", + "type": "asset" + } + ] + }, + { + "name": "setreserve", + "base": "", + "fields": [ + { + "name": "authority", + "type": "name" + }, + { + "name": "reserve_total", + "type": "asset" + }, + { + "name": "wire_equivalent", + "type": "asset" + } + ] + }, + { + "name": "slashop", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "name" + }, + { + "name": "reason", + "type": "string" + } + ] + }, + { + "name": "submitchain", + "base": "", + "fields": [ + { + "name": "operator_account", + "type": "name" + }, + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "epoch_hash", + "type": "checksum256" + }, + { + "name": "prev_epoch_hash", + "type": "checksum256" + }, + { + "name": "merkle_root", + "type": "checksum256" + }, + { + "name": "signature", + "type": "bytes" + } + ] + }, + { + "name": "underwrite_entry", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "operator_id", + "type": "uint64" + }, + { + "name": "status", + "type": "underwrite_status_t" + }, + { + "name": "source_amount", + "type": "asset" + }, + { + "name": "target_amount", + "type": "asset" + }, + { + "name": "source_chain", + "type": "chain_kind_t" + }, + { + "name": "target_chain", + "type": "chain_kind_t" + }, + { + "name": "exchange_rate_bps", + "type": "uint64" + }, + { + "name": "unlock_at", + "type": "time_point_sec" + }, + { + "name": "created_at", + "type": "time_point_sec" + }, + { + "name": "source_tx_hash", + "type": "checksum256" + }, + { + "name": "target_tx_hash", + "type": "checksum256" + } + ] + }, + { + "name": "unregop", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "name" + } + ] + }, + { + "name": "updreserve", + "base": "", + "fields": [ + { + "name": "operator_account", + "type": "name" + }, + { + "name": "token_sym", + "type": "symbol" + }, + { + "name": "delta", + "type": "int64" + } + ] + }, + { + "name": "uploadmsgs", + "base": "", + "fields": [ + { + "name": "operator_account", + "type": "name" + }, + { + "name": "epoch_number", + "type": "uint64" + }, + { + "name": "messages", + "type": "bytes" + }, + { + "name": "merkle_proofs", + "type": "bytes" + } + ] + }, + { + "name": "uwcancel", + "base": "", + "fields": [ + { + "name": "operator_account", + "type": "name" + }, + { + "name": "ledger_entry_id", + "type": "uint64" + }, + { + "name": "reason", + "type": "string" + } + ] + }, + { + "name": "uwconfirm", + "base": "", + "fields": [ + { + "name": "operator_account", + "type": "name" + }, + { + "name": "ledger_entry_id", + "type": "uint64" + } + ] + }, + { + "name": "uwexpire", + "base": "", + "fields": [] + }, + { + "name": "uwintent", + "base": "", + "fields": [ + { + "name": "underwriter", + "type": "name" + }, + { + "name": "message_id", + "type": "uint64" + }, + { + "name": "source_amount", + "type": "asset" + }, + { + "name": "target_amount", + "type": "asset" + }, + { + "name": "source_chain", + "type": "chain_kind_t" + }, + { + "name": "target_chain", + "type": "chain_kind_t" + }, + { + "name": "source_sig", + "type": "bytes" + }, + { + "name": "target_sig", + "type": "bytes" + } + ] + } + ], + "actions": [ + { + "name": "activateop", + "type": "activateop", + "ricardian_contract": "" + }, + { + "name": "bootstrap", + "type": "bootstrap", + "ricardian_contract": "" + }, + { + "name": "challenge", + "type": "challenge", + "ricardian_contract": "" + }, + { + "name": "chalresolve", + "type": "chalresolve", + "ricardian_contract": "" + }, + { + "name": "chalresp", + "type": "chalresp", + "ricardian_contract": "" + }, + { + "name": "chalvote", + "type": "chalvote", + "ricardian_contract": "" + }, + { + "name": "crank", + "type": "crank", + "ricardian_contract": "" + }, + { + "name": "emitchain", + "type": "emitchain", + "ricardian_contract": "" + }, + { + "name": "exitop", + "type": "exitop", + "ricardian_contract": "" + }, + { + "name": "getquote", + "type": "getquote", + "ricardian_contract": "" + }, + { + "name": "init", + "type": "init", + "ricardian_contract": "" + }, + { + "name": "oneshot", + "type": "oneshot", + "ricardian_contract": "" + }, + { + "name": "regoperator", + "type": "regoperator", + "ricardian_contract": "" + }, + { + "name": "setreserve", + "type": "setreserve", + "ricardian_contract": "" + }, + { + "name": "slashop", + "type": "slashop", + "ricardian_contract": "" + }, + { + "name": "submitchain", + "type": "submitchain", + "ricardian_contract": "" + }, + { + "name": "unregop", + "type": "unregop", + "ricardian_contract": "" + }, + { + "name": "updreserve", + "type": "updreserve", + "ricardian_contract": "" + }, + { + "name": "uploadmsgs", + "type": "uploadmsgs", + "ricardian_contract": "" + }, + { + "name": "uwcancel", + "type": "uwcancel", + "ricardian_contract": "" + }, + { + "name": "uwconfirm", + "type": "uwconfirm", + "ricardian_contract": "" + }, + { + "name": "uwexpire", + "type": "uwexpire", + "ricardian_contract": "" + }, + { + "name": "uwintent", + "type": "uwintent", + "ricardian_contract": "" + } + ], + "tables": [ + { + "name": "challenges", + "type": "challenge_info", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "depotstate", + "type": "depot_global_state", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "epochvotes", + "type": "epoch_vote", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "knownops", + "type": "known_operator", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "msgchains", + "type": "message_chain", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "oppepochin", + "type": "opp_epoch_in", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "oppepochout", + "type": "opp_epoch_out", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "oppforks", + "type": "opp_fork", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "oppforkvote", + "type": "opp_fork_vote", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "oppin", + "type": "opp_message_in", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "oppout", + "type": "opp_message_out", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "opschedule", + "type": "op_schedule", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "reserves", + "type": "reserve_balance", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "uwledger", + "type": "underwrite_entry", + "index_type": "i64", + "key_names": [], + "key_types": [] + } + ], + "ricardian_clauses": [], + "variants": [], + "action_results": [] +} \ No newline at end of file diff --git a/artifacts/sysio.depot.v2-handlers.wasm b/artifacts/sysio.depot.v2-handlers.wasm new file mode 100755 index 0000000000000000000000000000000000000000..f6635027756f968afd2f1e28418b681109c3d4d6 GIT binary patch literal 110570 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 + +#include +#include +#include + +#include +#include + + +namespace sysio { + + // ═══════════════════════════════════════════════════════════════════════════ + // Global depot state (singleton) + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("depotstate"), sysio::contract("sysio.depot")]] depot_global_state { + depot_state_t state = depot_state_active; + chain_kind_t chain_id = fc::crypto::chain_kind_unknown; + uint64_t current_epoch = 0; + uint64_t next_epoch = 1; + uint64_t next_msg_out = 0; // monotonic outbound message counter + time_point_sec last_crank_time; + name token_contract; // sysio.token or similar + bool initialized = false; + + SYSLIB_SERIALIZE(depot_global_state, + (state)(chain_id)(current_epoch)(next_epoch)(next_msg_out) + (last_crank_time)(token_contract)(initialized)) + }; + using depot_state_singleton = sysio::singleton<"depotstate"_n, depot_global_state>; + + // ═══════════════════════════════════════════════════════════════════════════ + // Known operators — scoped by chain_kind_t (FR-601) + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("knownops"), sysio::contract("sysio.depot")]] known_operator { + uint64_t id; + operator_type_t op_type; + operator_status_t status; + name wire_account; + std::vector secp256k1_pubkey; // 33 bytes compressed + std::vector ed25519_pubkey; // 32 bytes + asset collateral; + time_point_sec registered_at; + time_point_sec status_changed_at; + + uint64_t primary_key() const { return id; } + uint128_t by_type_status() const { return (uint128_t(op_type) << 64) | uint64_t(status); } + uint64_t by_account() const { return wire_account.value; } + checksum256 by_secp_pubkey() const { + return sha256(secp256k1_pubkey.data(), secp256k1_pubkey.size()); + } + }; + + using known_operators_table = multi_index<"knownops"_n, known_operator, + indexed_by<"bytypestatus"_n, const_mem_fun>, + indexed_by<"byaccount"_n, const_mem_fun>, + indexed_by<"bysecppub"_n, const_mem_fun> + >; + + // ═══════════════════════════════════════════════════════════════════════════ + // Operator election schedule — scoped by chain_kind_t (FR-604) + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("opschedule"), sysio::contract("sysio.depot")]] op_schedule { + uint64_t epoch_number; + std::vector elected_operator_ids; // MAX_BATCH_OPERATORS_PER_EPOCH entries + time_point_sec created_at; + + uint64_t primary_key() const { return epoch_number; } + }; + + using op_schedule_table = multi_index<"opschedule"_n, op_schedule>; + + // ═══════════════════════════════════════════════════════════════════════════ + // Message chain — scoped by chain_kind_t (FR-101) + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("msgchains"), sysio::contract("sysio.depot")]] message_chain { + uint64_t id; + message_direction_t direction; + chain_status_t status; + uint64_t epoch_number; + checksum256 merkle_root; + checksum256 epoch_hash; + checksum256 prev_epoch_hash; + std::vector payload; + std::vector operator_signature; + uint64_t operator_id; // ref → known_operator + time_point_sec created_at; + + uint64_t primary_key() const { return id; } + uint128_t by_epoch_dir() const { return (uint128_t(epoch_number) << 8) | uint64_t(direction); } + uint64_t by_status() const { return uint64_t(status); } + }; + + using message_chains_table = multi_index<"msgchains"_n, message_chain, + indexed_by<"byepochdir"_n, const_mem_fun>, + indexed_by<"bystatus"_n, const_mem_fun> + >; + + // ═══════════════════════════════════════════════════════════════════════════ + // Reserve balance state — scoped by chain_kind_t (FR-701) + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("reserves"), sysio::contract("sysio.depot")]] reserve_balance { + asset reserve_total; + asset wire_equivalent; + + uint64_t primary_key() const { return reserve_total.symbol.code().raw(); } + }; + + using reserves_table = multi_index<"reserves"_n, reserve_balance>; + + // ═══════════════════════════════════════════════════════════════════════════ + // Underwriting ledger — scoped by chain_kind_t (FR-401) + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("uwledger"), sysio::contract("sysio.depot")]] underwrite_entry { + uint64_t id; + uint64_t operator_id; + underwrite_status_t status; + asset source_amount; + asset target_amount; + chain_kind_t source_chain; + chain_kind_t target_chain; + uint64_t exchange_rate_bps; + time_point_sec unlock_at; + time_point_sec created_at; + checksum256 source_tx_hash; + checksum256 target_tx_hash; + + uint64_t primary_key() const { return id; } + uint64_t by_underwriter() const { return operator_id; } + uint64_t by_status() const { return uint64_t(status); } + uint64_t by_expiry() const { return unlock_at.sec_since_epoch(); } + }; + + using underwrite_table = multi_index<"uwledger"_n, underwrite_entry, + indexed_by<"byuw"_n, const_mem_fun>, + indexed_by<"bystatus"_n, const_mem_fun>, + indexed_by<"byexpiry"_n, const_mem_fun> + >; + + // ═══════════════════════════════════════════════════════════════════════════ + // Inbound epoch tracking — scoped by chain_kind_t + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("oppepochin"), sysio::contract("sysio.depot")]] opp_epoch_in { + uint64_t epoch_number; + uint64_t start_message; + uint64_t end_message; + checksum256 epoch_merkle; + bool challenge_flag = false; + + uint64_t primary_key() const { return epoch_number; } + }; + + using opp_epoch_in_table = multi_index<"oppepochin"_n, opp_epoch_in>; + + // ═══════════════════════════════════════════════════════════════════════════ + // Outbound epoch tracking — scoped by chain_kind_t + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("oppepochout"), sysio::contract("sysio.depot")]] opp_epoch_out { + uint64_t epoch_number; + uint64_t start_message; + uint64_t end_message; + checksum256 merkle_root; + + uint64_t primary_key() const { return epoch_number; } + }; + + using opp_epoch_out_table = multi_index<"oppepochout"_n, opp_epoch_out>; + + // ═══════════════════════════════════════════════════════════════════════════ + // Inbound message queue — scoped by chain_kind_t + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("oppin"), sysio::contract("sysio.depot")]] opp_message_in { + uint64_t message_number; + assertion_type_t assertion_type; + message_status_t status; + std::vector payload; + + uint64_t primary_key() const { return message_number; } + uint64_t by_status() const { return uint64_t(status); } + }; + + using opp_in_table = multi_index<"oppin"_n, opp_message_in, + indexed_by<"bystatus"_n, const_mem_fun> + >; + + // ═══════════════════════════════════════════════════════════════════════════ + // Outbound message queue — scoped by chain_kind_t + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("oppout"), sysio::contract("sysio.depot")]] opp_message_out { + uint64_t message_number; + assertion_type_t assertion_type; + std::vector payload; + + uint64_t primary_key() const { return message_number; } + }; + + using opp_out_table = multi_index<"oppout"_n, opp_message_out>; + + // ═══════════════════════════════════════════════════════════════════════════ + // Challenge tracking — scoped by chain_kind_t (FR-500) + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("challenges"), sysio::contract("sysio.depot")]] challenge_info { + uint64_t id; + uint64_t epoch_number; + challenge_status_t status; + uint8_t round; + std::vector challenge_data; + + uint64_t primary_key() const { return id; } + uint64_t by_epoch() const { return epoch_number; } + }; + + using challenges_table = multi_index<"challenges"_n, challenge_info, + indexed_by<"byepoch"_n, const_mem_fun> + >; + + // ═══════════════════════════════════════════════════════════════════════════ + // Epoch consensus vote tracking — scoped by chain_kind_t (FR-301) + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("epochvotes"), sysio::contract("sysio.depot")]] epoch_vote { + uint64_t id; + uint64_t epoch_number; + uint64_t operator_id; + checksum256 chain_hash; + time_point_sec submitted_at; + + uint64_t primary_key() const { return id; } + uint128_t by_epoch_op() const { return (uint128_t(epoch_number) << 64) | operator_id; } + uint64_t by_epoch() const { return epoch_number; } + }; + + using epoch_votes_table = multi_index<"epochvotes"_n, epoch_vote, + indexed_by<"byepochop"_n, const_mem_fun>, + indexed_by<"byepoch"_n, const_mem_fun> + >; + + // ═══════════════════════════════════════════════════════════════════════════ + // OPP fork tracking — scoped by chain_kind_t + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("oppforks"), sysio::contract("sysio.depot")]] opp_fork { + uint64_t fork_id; + uint64_t epoch_number; + uint64_t end_message_id; + checksum256 merkle_root; + + uint64_t primary_key() const { return fork_id; } + uint64_t by_epoch() const { return epoch_number; } + }; + + using opp_forks_table = multi_index<"oppforks"_n, opp_fork, + indexed_by<"byepoch"_n, const_mem_fun> + >; + + // ═══════════════════════════════════════════════════════════════════════════ + // OPP fork votes — scoped by chain_kind_t + // ═══════════════════════════════════════════════════════════════════════════ + struct [[sysio::table("oppforkvote"), sysio::contract("sysio.depot")]] opp_fork_vote { + uint64_t id; + uint64_t fork_id; + name voter; + uint8_t vote_state; // 0=pending, 1=accept, 2=reject + + uint64_t primary_key() const { return id; } + uint128_t by_fork_user() const { return (uint128_t(fork_id) << 64) | voter.value; } + }; + + using opp_fork_votes_table = multi_index<"oppforkvote"_n, opp_fork_vote, + indexed_by<"byforkuser"_n, const_mem_fun> + >; + +} // namespace sysio diff --git a/contracts/sysio.depot/include/sysio.depot/depot.types.hpp b/contracts/sysio.depot/include/sysio.depot/depot.types.hpp new file mode 100644 index 0000000..aad9992 --- /dev/null +++ b/contracts/sysio.depot/include/sysio.depot/depot.types.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include + +namespace sysio { + + using fc::crypto::chain_kind_t; + using fc::crypto::chain_key_type_t; + + // ── Message chain direction ────────────────────────────────────────────── + enum message_direction_t : uint8_t { + message_direction_inbound = 0, // Outpost -> Depot + message_direction_outbound = 1, // Depot -> Outpost + }; + + // ── Message chain lifecycle status ─────────────────────────────────────── + enum chain_status_t : uint8_t { + chain_status_created = 0, + chain_status_pending = 1, + chain_status_challenged = 2, + chain_status_valid = 3, + chain_status_slashed = 4, // terminal + }; + + // ── Individual message processing status ───────────────────────────────── + enum message_status_t : uint8_t { + message_status_pending = 0, // needs underwriting + message_status_ready = 1, // ready for processing + }; + + // ── Operator type ──────────────────────────────────────────────────────── + enum operator_type_t : uint8_t { + operator_type_node = 0, // node / producer + operator_type_batch = 1, + operator_type_underwriter = 2, + operator_type_challenger = 3, + }; + + // ── Operator lifecycle status ──────────────────────────────────────────── + enum operator_status_t : uint8_t { + operator_status_warmup = 0, + operator_status_active = 1, + operator_status_cooldown = 2, + operator_status_exited = 3, // graceful exit after cooldown + operator_status_slashed = 4, // terminal, collateral collapsed + }; + + // ── Challenge status ───────────────────────────────────────────────────── + enum challenge_status_t : uint8_t { + challenge_status_none = 0, + challenge_status_round1_pending = 1, + challenge_status_round1_complete = 2, + challenge_status_round2_pending = 3, + challenge_status_round2_complete = 4, + challenge_status_paused = 5, // global pause, awaiting manual + challenge_status_resolved = 6, + }; + + // ── Underwriting ledger entry status ───────────────────────────────────── + enum underwrite_status_t : uint8_t { + underwrite_status_intent_submitted = 0, + underwrite_status_intent_confirmed = 1, + underwrite_status_expired = 2, + underwrite_status_cancelled = 3, + }; + + // ── Depot global state ─────────────────────────────────────────────────── + enum depot_state_t : uint8_t { + depot_state_active = 0, + depot_state_challenge = 1, // normal msg processing suspended + depot_state_paused = 2, // global pause, manual intervention required + }; + + // ── OPP Assertion type IDs (from OPP Assertion Catalog) ────────────────── + enum assertion_type_t : uint16_t { + assertion_type_balance_sheet = 0xAA00, + assertion_type_stake_update = 0xEE00, + assertion_type_yield_reward = 0xEE01, + assertion_type_wire_purchase = 0xEE02, + assertion_type_operator_registration = 0xEE03, + assertion_type_challenge_response = 0xEE04, + assertion_type_slash_operator = 0xEE05, + }; + + // ── Constants ──────────────────────────────────────────────────────────── + static constexpr uint32_t MAX_BATCH_OPERATORS_PER_EPOCH = 7; + static constexpr uint32_t TOTAL_BATCH_OPERATORS = 21; + static constexpr uint32_t CONSENSUS_MAJORITY = 4; // of 7 + static constexpr uint32_t MAX_CHALLENGE_ROUNDS = 2; + + static constexpr uint32_t INTENT_LOCK_SECONDS = 6 * 3600; // 6 hours + static constexpr uint32_t CONFIRMED_LOCK_SECONDS = 24 * 3600; // 24 hours + + static constexpr uint64_t UNDERWRITE_FEE_BPS = 10; // 0.1% = 10 basis points + + // Slash distribution (basis points out of 10 000) + static constexpr uint64_t SLASH_CHALLENGER_BPS = 5000; // 50 % + static constexpr uint64_t SLASH_UNDERWRITERS_BPS = 2500; // 25 % + static constexpr uint64_t SLASH_BATCH_OPERATORS_BPS = 2500; // 25 % + + // Manual resolution vote threshold + static constexpr uint64_t RESOLUTION_VOTE_THRESHOLD_BPS = 6667; // ≈ 2/3 + +} // namespace sysio diff --git a/contracts/sysio.depot/include/sysio.depot/sysio.depot.hpp b/contracts/sysio.depot/include/sysio.depot/sysio.depot.hpp new file mode 100644 index 0000000..3728637 --- /dev/null +++ b/contracts/sysio.depot/include/sysio.depot/sysio.depot.hpp @@ -0,0 +1,166 @@ +#pragma once + +#include + +namespace sysio { + + class [[sysio::contract("sysio.depot")]] depot : public contract { + public: + using contract::contract; + + // ── Initialization ──────────────────────────────────────────────────── + [[sysio::action]] + void init(chain_kind_t chain_id, name token_contract); + + // ── Bootstrap (testnet only): seed initial epoch schedule ───────────── + [[sysio::action]] + void bootstrap(); + + // ── FR-800: Crank Execution ─────────────────────────────────────────── + [[sysio::action]] + void crank(name operator_account); + + // ── FR-100: OPP Inbound ─────────────────────────────────────────────── + [[sysio::action]] + void submitchain(name operator_account, + uint64_t epoch_number, + checksum256 epoch_hash, + checksum256 prev_epoch_hash, + checksum256 merkle_root, + std::vector signature); + + [[sysio::action]] + void uploadmsgs(name operator_account, + uint64_t epoch_number, + std::vector messages, + std::vector merkle_proofs); + + // ── FR-200: OPP Outbound ────────────────────────────────────────────── + [[sysio::action]] + void emitchain(name operator_account, uint64_t epoch_number); + + // ── FR-400: Underwriting ────────────────────────────────────────────── + [[sysio::action]] + void uwintent(name underwriter, + uint64_t message_id, + asset source_amount, + asset target_amount, + chain_kind_t source_chain, + chain_kind_t target_chain, + std::vector source_sig, + std::vector target_sig); + + [[sysio::action]] + void uwconfirm(name operator_account, uint64_t ledger_entry_id); + + [[sysio::action]] + void uwcancel(name operator_account, uint64_t ledger_entry_id, std::string reason); + + [[sysio::action]] + void uwexpire(); + + // ── FR-500: Challenge ───────────────────────────────────────────────── + [[sysio::action]] + void challenge(name challenger, uint64_t epoch_number, std::vector evidence); + + [[sysio::action]] + void chalresp(name operator_account, uint64_t challenge_id, std::vector response_data); + + [[sysio::action]] + void chalresolve(name proposer, + uint64_t challenge_id, + checksum256 original_hash, + checksum256 round1_hash, + checksum256 round2_hash); + + [[sysio::action]] + void chalvote(name voter, uint64_t challenge_id, bool approve); + + // ── FR-600: Operator Lifecycle ──────────────────────────────────────── + [[sysio::action]] + void regoperator(name wire_account, + operator_type_t op_type, + std::vector secp256k1_pubkey, + std::vector ed25519_pubkey, + asset collateral); + + [[sysio::action]] + void unregop(name wire_account); + + [[sysio::action]] + void activateop(name wire_account); + + [[sysio::action]] + void exitop(name wire_account); + + [[sysio::action]] + void slashop(name wire_account, std::string reason); + + // ── FR-700: Reserve & Swap ──────────────────────────────────────────── + [[sysio::action]] + void setreserve(name authority, asset reserve_total, asset wire_equivalent); + + [[sysio::action]] + void updreserve(name operator_account, symbol token_sym, int64_t delta); + + // ── FR-900: Swap Quote (read-only) ──────────────────────────────────── + [[sysio::action]] + void getquote(symbol source_sym, symbol target_sym, asset amount); + + // ── FR-1000: Oneshot Warrant Conversion ─────────────────────────────── + [[sysio::action]] + void oneshot(name beneficiary, asset amount); + + // ── Action wrappers ─────────────────────────────────────────────────── + using init_action = action_wrapper<"init"_n, &depot::init>; + using bootstrap_action = action_wrapper<"bootstrap"_n, &depot::bootstrap>; + using crank_action = action_wrapper<"crank"_n, &depot::crank>; + using submitchain_action = action_wrapper<"submitchain"_n, &depot::submitchain>; + using uploadmsgs_action = action_wrapper<"uploadmsgs"_n, &depot::uploadmsgs>; + using emitchain_action = action_wrapper<"emitchain"_n, &depot::emitchain>; + using uwintent_action = action_wrapper<"uwintent"_n, &depot::uwintent>; + using uwconfirm_action = action_wrapper<"uwconfirm"_n, &depot::uwconfirm>; + using uwcancel_action = action_wrapper<"uwcancel"_n, &depot::uwcancel>; + using uwexpire_action = action_wrapper<"uwexpire"_n, &depot::uwexpire>; + using challenge_action = action_wrapper<"challenge"_n, &depot::challenge>; + using chalresp_action = action_wrapper<"chalresp"_n, &depot::chalresp>; + using chalresolve_action = action_wrapper<"chalresolve"_n, &depot::chalresolve>; + using chalvote_action = action_wrapper<"chalvote"_n, &depot::chalvote>; + using regoperator_action = action_wrapper<"regoperator"_n, &depot::regoperator>; + using unregop_action = action_wrapper<"unregop"_n, &depot::unregop>; + using activateop_action = action_wrapper<"activateop"_n, &depot::activateop>; + using exitop_action = action_wrapper<"exitop"_n, &depot::exitop>; + using slashop_action = action_wrapper<"slashop"_n, &depot::slashop>; + using setreserve_action = action_wrapper<"setreserve"_n, &depot::setreserve>; + using updreserve_action = action_wrapper<"updreserve"_n, &depot::updreserve>; + using getquote_action = action_wrapper<"getquote"_n, &depot::getquote>; + using oneshot_action = action_wrapper<"oneshot"_n, &depot::oneshot>; + + private: + // ── Internal helpers ────────────────────────────────────────────────── + depot_global_state get_state(); + void set_state(const depot_global_state& s); + + void verify_elected(name operator_account, uint64_t epoch_number); + void evaluate_consensus(uint64_t epoch_number); + void process_ready_messages(); + void expire_underwriting_locks(); + void elect_operators_for_epoch(uint64_t next_epoch); + void process_assertion(uint64_t message_number, assertion_type_t type, const std::vector& payload); + void queue_outbound_message(assertion_type_t type, const std::vector& payload); + void mark_epoch_valid(uint64_t chain_scope, uint64_t epoch_number); + void slash_minority_operators(uint64_t chain_scope, uint64_t epoch_number, + const checksum256& consensus_hash); + void enter_challenge_state(uint64_t epoch_number); + void resume_from_challenge(); + void distribute_slash(uint64_t operator_id, name challenger); + uint64_t calculate_swap_rate(symbol source, symbol target, asset amount); + bool check_rate_threshold(uint64_t rate_bps, uint64_t threshold_bps); + + uint64_t scope() { + auto s = get_state(); + return uint64_t(s.chain_id); + } + }; + +} // namespace sysio diff --git a/contracts/sysio.depot/src/challenge.cpp b/contracts/sysio.depot/src/challenge.cpp new file mode 100644 index 0000000..b83ba69 --- /dev/null +++ b/contracts/sysio.depot/src/challenge.cpp @@ -0,0 +1,352 @@ +#include + +namespace sysio { + +// ── challenge (FR-501/502) ────────────────────────────────────────────────── +// +// A challenger submits a challenge against an epoch. +// Suspends normal message processing, enters CHALLENGE state, +// queues a CHALLENGE_REQUEST outbound message for the Outpost. + +void depot::challenge(name challenger, uint64_t epoch_number, std::vector evidence) { + require_auth(challenger); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + check(s.state == depot_state_active || s.state == depot_state_challenge, + "depot: system is paused, cannot accept challenges"); + + // Verify the challenger is a registered challenger operator + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto op_it = by_account.find(challenger.value); + check(op_it != by_account.end(), "depot: challenger not registered"); + check(op_it->op_type == operator_type_challenger, "depot: account is not a challenger"); + check(op_it->status == operator_status_active, "depot: challenger is not active"); + + // Verify epoch exists and is in a challengeable state + opp_epoch_in_table epochs(get_self(), chain_scope); + auto ep_it = epochs.find(epoch_number); + check(ep_it != epochs.end(), "depot: epoch not found"); + check(!ep_it->challenge_flag, "depot: epoch already under challenge"); + + // Check no active challenge exists for this epoch + challenges_table chals(get_self(), chain_scope); + auto by_epoch = chals.get_index<"byepoch"_n>(); + auto chal_it = by_epoch.find(epoch_number); + if (chal_it != by_epoch.end()) { + check(chal_it->status == challenge_status_resolved, + "depot: epoch already has an active challenge"); + } + + // Create challenge record + chals.emplace(get_self(), [&](auto& c) { + c.id = chals.available_primary_key(); + c.epoch_number = epoch_number; + c.status = challenge_status_round1_pending; + c.round = 1; + c.challenge_data = evidence; + }); + + // Mark epoch as challenged + epochs.modify(ep_it, same_payer, [&](auto& e) { + e.challenge_flag = true; + }); + + // Enter challenge state + enter_challenge_state(epoch_number); + + // FR-502: Queue outbound CHALLENGE_REQUEST to Outpost + queue_outbound_message(assertion_type_challenge_response, evidence); +} + +// ── chalresp (FR-503/504) ─────────────────────────────────────────────────── +// +// An elected batch operator responds to a challenge. +// If this is round 1, compare with original. If mismatch on round 2, PAUSE. + +void depot::chalresp(name operator_account, uint64_t challenge_id, + std::vector response_data) { + require_auth(operator_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + check(s.state == depot_state_challenge, "depot: system is not in challenge state"); + + // Verify operator is elected + verify_elected(operator_account, s.current_epoch); + + // Find the challenge + challenges_table chals(get_self(), chain_scope); + auto chal_it = chals.find(challenge_id); + check(chal_it != chals.end(), "depot: challenge not found"); + check(chal_it->status == challenge_status_round1_pending || + chal_it->status == challenge_status_round2_pending, + "depot: challenge is not awaiting a response"); + + uint8_t current_round = chal_it->round; + + // Compute hash of the response data + checksum256 response_hash = sha256(response_data.data(), response_data.size()); + + // Compare with the original epoch data + opp_epoch_in_table epochs(get_self(), chain_scope); + auto ep_it = epochs.find(chal_it->epoch_number); + check(ep_it != epochs.end(), "depot: challenged epoch not found"); + + bool hashes_match = (response_hash == ep_it->epoch_merkle); + + if (current_round == 1) { + if (hashes_match) { + // FR-503: Round 1 — response matches original, challenge fails + chals.modify(chal_it, same_payer, [&](auto& c) { + c.status = challenge_status_round1_complete; + }); + + // Resume normal operations + resume_from_challenge(); + } else { + // FR-504: Round 1 — mismatch, escalate to round 2 + chals.modify(chal_it, same_payer, [&](auto& c) { + c.status = challenge_status_round2_pending; + c.round = 2; + }); + + // Queue second challenge request + queue_outbound_message(assertion_type_challenge_response, response_data); + } + } else if (current_round == 2) { + if (hashes_match) { + // FR-505: Round 2 — response matches, original was wrong + chals.modify(chal_it, same_payer, [&](auto& c) { + c.status = challenge_status_round2_complete; + }); + + // Slash original operators who delivered wrong data + epoch_votes_table votes(get_self(), chain_scope); + auto by_epoch = votes.get_index<"byepoch"_n>(); + auto vit = by_epoch.lower_bound(chal_it->epoch_number); + while (vit != by_epoch.end() && vit->epoch_number == chal_it->epoch_number) { + if (vit->chain_hash != response_hash) { + // Slash this operator + known_operators_table ops2(get_self(), chain_scope); + auto op2 = ops2.find(vit->operator_id); + if (op2 != ops2.end() && op2->status != operator_status_slashed) { + ops2.modify(op2, same_payer, [&](auto& o) { + o.status = operator_status_slashed; + o.collateral.amount = 0; + o.status_changed_at = current_time_point(); + }); + } + } + ++vit; + } + + // Resume with corrected data + resume_from_challenge(); + } else { + // FR-506: Round 2 — second mismatch, enter PAUSE state + chals.modify(chal_it, same_payer, [&](auto& c) { + c.status = challenge_status_paused; + }); + + // Enter global pause — manual resolution required + auto state = get_state(); + state.state = depot_state_paused; + set_state(state); + } + } +} + +// ── chalresolve (FR-507) ──────────────────────────────────────────────────── +// +// Manual resolution: A T1/T2 assembler proposes a resolution with +// 3 hashes (original, round1, round2). Requires 2/3 supermajority vote +// from active node operators to accept. + +void depot::chalresolve(name proposer, + uint64_t challenge_id, + checksum256 original_hash, + checksum256 round1_hash, + checksum256 round2_hash) { + require_auth(proposer); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + check(s.state == depot_state_paused, "depot: system must be paused for manual resolution"); + + challenges_table chals(get_self(), chain_scope); + auto chal_it = chals.find(challenge_id); + check(chal_it != chals.end(), "depot: challenge not found"); + check(chal_it->status == challenge_status_paused, + "depot: challenge must be in paused state for resolution"); + + // Create a fork proposal for voting + opp_forks_table forks(get_self(), chain_scope); + uint64_t fork_id = forks.available_primary_key(); + + // The merkle root here represents the "correct" chain hash as determined + // by the resolver's analysis of original, round1, and round2 hashes + forks.emplace(get_self(), [&](auto& f) { + f.fork_id = fork_id; + f.epoch_number = chal_it->epoch_number; + f.end_message_id = 0; // will be determined after vote passes + f.merkle_root = round2_hash; // the latest re-derivation is proposed as correct + }); +} + +// ── chalvote (FR-507) ─────────────────────────────────────────────────────── +// +// Active node operators vote on a manual resolution proposal. +// If 2/3 supermajority approves, the resolution is applied: +// slash losers, resume operations. + +void depot::chalvote(name voter, uint64_t challenge_id, bool approve) { + require_auth(voter); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + check(s.state == depot_state_paused, "depot: system must be paused for voting"); + + // Verify voter is an active node operator + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto op_it = by_account.find(voter.value); + check(op_it != by_account.end(), "depot: voter not registered as operator"); + check(op_it->op_type == operator_type_node, "depot: only node operators can vote on resolutions"); + check(op_it->status == operator_status_active, "depot: voter must be active"); + + // Find the challenge and associated fork + challenges_table chals(get_self(), chain_scope); + auto chal_it = chals.find(challenge_id); + check(chal_it != chals.end(), "depot: challenge not found"); + check(chal_it->status == challenge_status_paused, "depot: challenge not in voting state"); + + // Find the fork proposal for this epoch + opp_forks_table forks(get_self(), chain_scope); + auto by_epoch = forks.get_index<"byepoch"_n>(); + auto fork_it = by_epoch.find(chal_it->epoch_number); + check(fork_it != by_epoch.end(), "depot: no resolution proposal found for this epoch"); + + // Check voter hasn't already voted on this fork + opp_fork_votes_table votes(get_self(), chain_scope); + auto by_fu = votes.get_index<"byforkuser"_n>(); + uint128_t fu_key = (uint128_t(fork_it->fork_id) << 64) | voter.value; + check(by_fu.find(fu_key) == by_fu.end(), "depot: already voted on this resolution"); + + // Record vote + votes.emplace(get_self(), [&](auto& v) { + v.id = votes.available_primary_key(); + v.fork_id = fork_it->fork_id; + v.voter = voter; + v.vote_state = approve ? 1 : 2; + }); + + // Count votes for this fork + auto vit = by_fu.lower_bound(uint128_t(fork_it->fork_id) << 64); + uint32_t accept_count = 0; + uint32_t total_count = 0; + while (vit != by_fu.end()) { + // Extract fork_id from composite key + uint64_t vid_fork = uint64_t(vit->by_fork_user() >> 64); + if (vid_fork != fork_it->fork_id) break; + + ++total_count; + if (vit->vote_state == 1) ++accept_count; + ++vit; + } + + // Count total active node operators for threshold calculation + auto by_ts = ops.get_index<"bytypestatus"_n>(); + uint128_t node_key = (uint128_t(operator_type_node) << 64) | uint64_t(operator_status_active); + auto node_it = by_ts.lower_bound(node_key); + uint32_t total_nodes = 0; + while (node_it != by_ts.end() && + node_it->op_type == operator_type_node && + node_it->status == operator_status_active) { + ++total_nodes; + ++node_it; + } + + // FR-507: Check if 2/3 supermajority is reached + if (total_nodes > 0 && accept_count * 10000 >= total_nodes * RESOLUTION_VOTE_THRESHOLD_BPS) { + // Resolution accepted — apply it + chals.modify(chal_it, same_payer, [&](auto& c) { + c.status = challenge_status_resolved; + }); + + // Slash operators who delivered non-consensus data + slash_minority_operators(chain_scope, chal_it->epoch_number, fork_it->merkle_root); + + // Resume operations + resume_from_challenge(); + } +} + +// ── enter_challenge_state (internal) ──────────────────────────────────────── + +void depot::enter_challenge_state(uint64_t epoch_number) { + auto s = get_state(); + if (s.state != depot_state_challenge) { + s.state = depot_state_challenge; + set_state(s); + } +} + +// ── resume_from_challenge (internal) ──────────────────────────────────────── + +void depot::resume_from_challenge() { + auto s = get_state(); + s.state = depot_state_active; + set_state(s); +} + +// ── distribute_slash (internal, FR-504/DEC-005) ───────────────────────────── +// +// Distributes slashed collateral: +// 50% to the challenger +// 25% to underwriters +// 25% to batch operators + +void depot::distribute_slash(uint64_t operator_id, name challenger) { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + known_operators_table ops(get_self(), chain_scope); + auto op_it = ops.find(operator_id); + if (op_it == ops.end()) return; + + int64_t total_collateral = op_it->collateral.amount; + if (total_collateral <= 0) return; + + symbol collateral_sym = op_it->collateral.symbol; + + // Calculate shares + int64_t challenger_share = (total_collateral * SLASH_CHALLENGER_BPS) / 10000; + int64_t underwriter_share = (total_collateral * SLASH_UNDERWRITERS_BPS) / 10000; + int64_t batch_share = total_collateral - challenger_share - underwriter_share; + + // Zero the operator's collateral + ops.modify(op_it, same_payer, [&](auto& o) { + o.collateral.amount = 0; + o.status = operator_status_slashed; + o.status_changed_at = current_time_point(); + }); + + // TODO: Transfer shares via inline actions to sysio.token + // action(permission_level{get_self(), "active"_n}, + // s.token_contract, "transfer"_n, + // std::make_tuple(get_self(), challenger, + // asset(challenger_share, collateral_sym), + // std::string("slash distribution - challenger"))) + // .send(); + // + // Underwriter and batch operator shares would need to be distributed + // proportionally among all active operators of those types. +} + +} // namespace sysio diff --git a/contracts/sysio.depot/src/consensus.cpp b/contracts/sysio.depot/src/consensus.cpp new file mode 100644 index 0000000..de99612 --- /dev/null +++ b/contracts/sysio.depot/src/consensus.cpp @@ -0,0 +1,146 @@ +#include + +namespace sysio { + +// ── evaluate_consensus (FR-301/302/303/304) ────────────────────────────────── +// +// Collects epoch votes from all elected batch operators and determines +// consensus via Option A (all 7 identical) or Option B (4+ majority at +// epoch boundary with all delivered chains matching). + +void depot::evaluate_consensus(uint64_t epoch_number) { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + // Skip if already in challenge or paused state for this epoch + if (s.state == depot_state_paused) return; + + // Get the elected schedule for this epoch + op_schedule_table sched(get_self(), chain_scope); + auto sch_it = sched.find(epoch_number); + if (sch_it == sched.end()) return; // no schedule yet + + uint32_t total_elected = sch_it->elected_operator_ids.size(); + + // Collect all votes for this epoch + epoch_votes_table votes(get_self(), chain_scope); + auto by_epoch = votes.get_index<"byepoch"_n>(); + auto vit = by_epoch.lower_bound(epoch_number); + + // Count votes per distinct hash + struct hash_count { + checksum256 hash; + uint32_t count = 0; + }; + std::vector hash_counts; + uint32_t total_votes = 0; + + while (vit != by_epoch.end() && vit->epoch_number == epoch_number) { + ++total_votes; + bool found = false; + for (auto& hc : hash_counts) { + if (hc.hash == vit->chain_hash) { + ++hc.count; + found = true; + break; + } + } + if (!found) { + hash_counts.push_back({vit->chain_hash, 1}); + } + ++vit; + } + + if (total_votes == 0) return; // no votes yet + + // ── Option A: All elected operators delivered identical hashes ───────── + if (total_votes == total_elected && hash_counts.size() == 1) { + // Unanimous consensus achieved + mark_epoch_valid(chain_scope, epoch_number); + return; + } + + // ── Option B: Majority (4+ of 7) with identical data ───────────────── + // Only viable when epoch boundary is reached without all operators delivering + if (total_votes >= CONSENSUS_MAJORITY) { + for (auto& hc : hash_counts) { + if (hc.count >= CONSENSUS_MAJORITY) { + // Majority consensus — check that this is the only hash that + // was delivered (all delivered chains match) + // Per FR-302 Option B: majority deliver identical AND + // all delivered chains match + if (hash_counts.size() == 1) { + mark_epoch_valid(chain_scope, epoch_number); + return; + } + // If there are multiple distinct hashes but majority agrees, + // we still have consensus but with dissenters to slash + mark_epoch_valid(chain_scope, epoch_number); + // Identify and slash minority operators + slash_minority_operators(chain_scope, epoch_number, hc.hash); + return; + } + } + } + + // ── Not enough votes yet — wait for more submissions ────────────────── + // If all elected have voted and no consensus, trigger challenge + if (total_votes == total_elected) { + enter_challenge_state(epoch_number); + } +} + +// ── mark_epoch_valid (internal) ────────────────────────────────────────────── + +void depot::mark_epoch_valid(uint64_t chain_scope, uint64_t epoch_number) { + message_chains_table chains(get_self(), chain_scope); + auto by_epoch = chains.get_index<"byepochdir"_n>(); + uint128_t ek = (uint128_t(epoch_number) << 8) | uint64_t(message_direction_inbound); + auto it = by_epoch.lower_bound(ek); + + while (it != by_epoch.end() && + it->epoch_number == epoch_number && + it->direction == message_direction_inbound) { + if (it->status == chain_status_pending) { + by_epoch.modify(it, same_payer, [&](auto& c) { + c.status = chain_status_valid; + }); + } + ++it; + } +} + +// ── slash_minority_operators (internal) ────────────────────────────────────── +// +// After consensus, identify operators who delivered non-consensus hashes +// and slash them (FR-504). + +void depot::slash_minority_operators(uint64_t chain_scope, uint64_t epoch_number, + const checksum256& consensus_hash) { + epoch_votes_table votes(get_self(), chain_scope); + auto by_epoch = votes.get_index<"byepoch"_n>(); + auto vit = by_epoch.lower_bound(epoch_number); + + known_operators_table ops(get_self(), chain_scope); + + while (vit != by_epoch.end() && vit->epoch_number == epoch_number) { + if (vit->chain_hash != consensus_hash) { + // This operator delivered a non-consensus hash — slash them + auto op_it = ops.find(vit->operator_id); + if (op_it != ops.end() && op_it->status != operator_status_slashed) { + ops.modify(op_it, same_payer, [&](auto& o) { + o.status = operator_status_slashed; + o.collateral.amount = 0; // DEC-006 + o.status_changed_at = current_time_point(); + }); + + // Queue outbound slash to Outpost + std::vector slash_payload; + queue_outbound_message(assertion_type_slash_operator, slash_payload); + } + } + ++vit; + } +} + +} // namespace sysio diff --git a/contracts/sysio.depot/src/operators.cpp b/contracts/sysio.depot/src/operators.cpp new file mode 100644 index 0000000..14d74ef --- /dev/null +++ b/contracts/sysio.depot/src/operators.cpp @@ -0,0 +1,248 @@ +#include +#include + +namespace sysio { + +// ── regoperator (FR-602) ───────────────────────────────────────────────────── + +void depot::regoperator(name wire_account, + operator_type_t op_type, + std::vector secp256k1_pubkey, + std::vector ed25519_pubkey, + asset collateral) { + require_auth(wire_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + // Validate key sizes (FR-606) + check(secp256k1_pubkey.size() == 33, "depot: secp256k1 pubkey must be 33 bytes compressed"); + check(ed25519_pubkey.size() == 32, "depot: ed25519 pubkey must be 32 bytes"); + check(collateral.amount > 0, "depot: collateral must be positive"); + check(op_type == operator_type_node || op_type == operator_type_batch || + op_type == operator_type_underwriter || op_type == operator_type_challenger, + "depot: invalid operator type"); + + known_operators_table ops(get_self(), chain_scope); + + // Check no duplicate registration for this account + auto by_account = ops.get_index<"byaccount"_n>(); + auto acct_it = by_account.find(wire_account.value); + check(acct_it == by_account.end(), "depot: operator already registered for this chain"); + + // Check no duplicate secp256k1 pubkey + auto by_secp = ops.get_index<"bysecppub"_n>(); + auto secp_hash = sha256(secp256k1_pubkey.data(), secp256k1_pubkey.size()); + check(by_secp.find(secp_hash) == by_secp.end(), + "depot: secp256k1 pubkey already registered"); + + ops.emplace(get_self(), [&](auto& o) { + o.id = ops.available_primary_key(); + o.op_type = op_type; + o.status = operator_status_warmup; + o.wire_account = wire_account; + o.secp256k1_pubkey = secp256k1_pubkey; + o.ed25519_pubkey = ed25519_pubkey; + o.collateral = collateral; + o.registered_at = current_time_point(); + o.status_changed_at = current_time_point(); + }); +} + +// ── unregop ────────────────────────────────────────────────────────────────── + +void depot::unregop(name wire_account) { + require_auth(wire_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto it = by_account.find(wire_account.value); + check(it != by_account.end(), "depot: operator not found"); + check(it->status != operator_status_slashed, "depot: operator already slashed"); + check(it->status != operator_status_exited, "depot: operator already exited"); + check(it->status != operator_status_cooldown, "depot: operator already in cooldown"); + check(it->status == operator_status_active || it->status == operator_status_warmup, + "depot: operator must be active or in warmup to unregister"); + + by_account.modify(it, same_payer, [&](auto& o) { + o.status = operator_status_cooldown; + o.status_changed_at = current_time_point(); + }); +} + +// ── exitop (cooldown -> exited, graceful exit) ─────────────────────────────── + +void depot::exitop(name wire_account) { + require_auth(wire_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto it = by_account.find(wire_account.value); + check(it != by_account.end(), "depot: operator not found"); + check(it->status == operator_status_cooldown, "depot: operator must be in cooldown to exit"); + + // TODO: Verify cooldown duration has elapsed (OQ-010: durations not yet specified) + + by_account.modify(it, same_payer, [&](auto& o) { + o.status = operator_status_exited; + o.status_changed_at = current_time_point(); + }); + + // TODO: Return collateral to operator's wire_account +} + +// ── activateop ─────────────────────────────────────────────────────────────── + +void depot::activateop(name wire_account) { + require_auth(get_self()); // governance action + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto it = by_account.find(wire_account.value); + check(it != by_account.end(), "depot: operator not found"); + check(it->status == operator_status_warmup, "depot: operator must be in warmup state"); + + by_account.modify(it, same_payer, [&](auto& o) { + o.status = operator_status_active; + o.status_changed_at = current_time_point(); + }); +} + +// ── slashop (FR-605 / FR-504) ──────────────────────────────────────────────── + +void depot::slashop(name wire_account, std::string reason) { + require_auth(get_self()); // only callable internally or by governance + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto it = by_account.find(wire_account.value); + check(it != by_account.end(), "depot: operator not found"); + check(it->status != operator_status_slashed, "depot: operator already slashed"); + + by_account.modify(it, same_payer, [&](auto& o) { + o.status = operator_status_slashed; + o.collateral.amount = 0; // DEC-006: collapse ALL collateral + o.status_changed_at = current_time_point(); + }); + + // Queue outbound slash notification to Outpost (FR-504 step 6) + // Roster update with reason BAD_BEHAVIOUR + std::vector slash_payload; + // Serialize: operator_id + reason + // The exact serialization format will be defined by OPP spec + queue_outbound_message(assertion_type_slash_operator, slash_payload); +} + +// ── elect_operators_for_epoch (FR-604) ─────────────────────────────────────── + +void depot::elect_operators_for_epoch(uint64_t next_epoch) { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + // Check if schedule already exists for this epoch + op_schedule_table sched(get_self(), chain_scope); + auto existing = sched.find(next_epoch); + if (existing != sched.end()) return; // already elected + + // Gather all active batch operators + known_operators_table ops(get_self(), chain_scope); + auto by_ts = ops.get_index<"bytypestatus"_n>(); + uint128_t key = (uint128_t(operator_type_batch) << 64) | uint64_t(operator_status_active); + auto it = by_ts.lower_bound(key); + + std::vector active_ids; + while (it != by_ts.end() && it->op_type == operator_type_batch && it->status == operator_status_active) { + active_ids.push_back(it->id); + ++it; + } + + check(active_ids.size() >= MAX_BATCH_OPERATORS_PER_EPOCH, + "depot: insufficient active batch operators for election"); + + // Get previous epoch schedule to enforce no-consecutive rule + std::vector prev_elected; + if (next_epoch > 1) { + auto prev = sched.find(next_epoch - 1); + if (prev != sched.end()) { + prev_elected = prev->elected_operator_ids; + } + } + + // Random round-robin selection (FR-604) + // Use block hash as randomness source for deterministic on-chain election + // tapos_block_prefix gives us block-specific entropy + uint32_t seed = sysio::tapos_block_prefix(); + std::vector eligible; + for (auto id : active_ids) { + // No consecutive participation rule + bool was_previous = false; + for (auto prev_id : prev_elected) { + if (prev_id == id) { was_previous = true; break; } + } + if (!was_previous) { + eligible.push_back(id); + } + } + + // If not enough eligible due to no-consecutive rule, relax it + if (eligible.size() < MAX_BATCH_OPERATORS_PER_EPOCH) { + eligible = active_ids; + } + + // Fisher-Yates shuffle using deterministic seed + for (uint32_t i = eligible.size() - 1; i > 0; --i) { + seed = seed * 1103515245 + 12345; // LCG + uint32_t j = seed % (i + 1); + auto tmp = eligible[i]; + eligible[i] = eligible[j]; + eligible[j] = tmp; + } + + // Take first MAX_BATCH_OPERATORS_PER_EPOCH + std::vector elected(eligible.begin(), + eligible.begin() + std::min((uint32_t)eligible.size(), MAX_BATCH_OPERATORS_PER_EPOCH)); + + sched.emplace(get_self(), [&](auto& r) { + r.epoch_number = next_epoch; + r.elected_operator_ids = elected; + r.created_at = current_time_point(); + }); +} + +// ── verify_elected (internal) ──────────────────────────────────────────────── + +void depot::verify_elected(name operator_account, uint64_t epoch_number) { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + // Find operator id + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto op_it = by_account.find(operator_account.value); + check(op_it != by_account.end(), "depot: operator not registered"); + + // Find schedule for this epoch + op_schedule_table sched(get_self(), chain_scope); + auto sch_it = sched.find(epoch_number); + check(sch_it != sched.end(), "depot: no schedule for epoch"); + + bool found = false; + for (auto id : sch_it->elected_operator_ids) { + if (id == op_it->id) { found = true; break; } + } + check(found, "depot: operator not elected for this epoch (FR-605: non-elected delivery => slash)"); +} + +} // namespace sysio diff --git a/contracts/sysio.depot/src/opp_inbound.cpp b/contracts/sysio.depot/src/opp_inbound.cpp new file mode 100644 index 0000000..403413d --- /dev/null +++ b/contracts/sysio.depot/src/opp_inbound.cpp @@ -0,0 +1,219 @@ +#include + +namespace sysio { + +// ── submitchain (FR-101/103/104/105) ───────────────────────────────────────── +// +// Batch operator submits an epoch envelope + signature. +// The Depot collects these as votes; consensus is evaluated once enough arrive. + +void depot::submitchain(name operator_account, + uint64_t epoch_number, + checksum256 epoch_hash, + checksum256 prev_epoch_hash, + checksum256 merkle_root, + std::vector signature) { + require_auth(operator_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + // FR-103: Verify operator is elected for this epoch + verify_elected(operator_account, epoch_number); + + // FR-102: Sequential epoch processing + check(epoch_number == s.current_epoch || epoch_number == s.current_epoch + 1, + "depot: epoch out of order (FR-102: must be processed sequentially)"); + + // Resolve operator id + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto op_it = by_account.find(operator_account.value); + // verify_elected already checks this, but be explicit + check(op_it != by_account.end(), "depot: operator not registered"); + + // FR-105: Validate previous epoch hash against stored value + if (epoch_number > 0) { + opp_epoch_in_table epochs(get_self(), chain_scope); + auto prev_epoch = epochs.find(epoch_number - 1); + if (prev_epoch != epochs.end()) { + check(prev_epoch_hash == prev_epoch->epoch_merkle, + "depot: prev_epoch_hash does not match stored epoch merkle (FR-105)"); + } + } + + // FR-104: Validate signature (recover signer, confirm it matches operator) + // The signature is over the epoch_hash using the operator's secp256k1 key. + // On-chain recovery: assert_recover_key(epoch_hash, sig, expected_pubkey) + // For now we store the signature; full recovery requires the CDT + // assert_recover_key intrinsic which takes a digest + sig + pubkey. + // TODO: Uncomment when OPP signature format is finalized + // public_key recovered = recover_key(epoch_hash, signature); + // check(recovered matches op_it->secp256k1_pubkey) + + // Store this operator's vote for the epoch + epoch_votes_table votes(get_self(), chain_scope); + auto by_eo = votes.get_index<"byepochop"_n>(); + uint128_t eo_key = (uint128_t(epoch_number) << 64) | op_it->id; + check(by_eo.find(eo_key) == by_eo.end(), + "depot: operator already submitted for this epoch"); + + votes.emplace(get_self(), [&](auto& v) { + v.id = votes.available_primary_key(); + v.epoch_number = epoch_number; + v.operator_id = op_it->id; + v.chain_hash = epoch_hash; + v.submitted_at = current_time_point(); + }); + + // Store the message chain record + message_chains_table chains(get_self(), chain_scope); + chains.emplace(get_self(), [&](auto& c) { + c.id = chains.available_primary_key(); + c.direction = message_direction_inbound; + c.status = chain_status_pending; + c.epoch_number = epoch_number; + c.merkle_root = merkle_root; + c.epoch_hash = epoch_hash; + c.prev_epoch_hash = prev_epoch_hash; + c.operator_signature = signature; + c.operator_id = op_it->id; + c.created_at = current_time_point(); + }); + + // Attempt consensus evaluation after each submission + evaluate_consensus(epoch_number); +} + +// ── uploadmsgs (FR-106/107/108) ────────────────────────────────────────────── +// +// After consensus is reached, a single chosen operator uploads the actual +// messages with merkle proofs linking message IDs to the epoch merkle root. + +void depot::uploadmsgs(name operator_account, + uint64_t epoch_number, + std::vector messages, + std::vector merkle_proofs) { + require_auth(operator_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + // Verify operator is elected + verify_elected(operator_account, epoch_number); + + // Verify the epoch has reached consensus (status = valid) + message_chains_table chains(get_self(), chain_scope); + auto by_epoch = chains.get_index<"byepochdir"_n>(); + uint128_t ek = (uint128_t(epoch_number) << 8) | uint64_t(message_direction_inbound); + auto chain_it = by_epoch.lower_bound(ek); + + bool has_valid = false; + while (chain_it != by_epoch.end() && + chain_it->epoch_number == epoch_number && + chain_it->direction == message_direction_inbound) { + if (chain_it->status == chain_status_valid) { + has_valid = true; + break; + } + ++chain_it; + } + check(has_valid, "depot: epoch has not reached consensus yet"); + + // FR-106: Unpack message chain attestations + // Messages format: [assertion_type(2) | payload_len(4) | payload(N)] repeated + // Merkle proofs: [proof per message for verification against epoch merkle root] + // + // For each message: + // 1. Recompute message ID from message data (FR-106) + // 2. Evaluate merkle proof against epoch merkle root + // 3. Classify as NORMAL or CHALLENGE (FR-106) + // 4. Assign status PENDING or READY (FR-107) + + opp_in_table in_msgs(get_self(), chain_scope); + + // Deserialize messages + const char* ptr = messages.data(); + const char* end = ptr + messages.size(); + uint64_t msg_num = 0; + + // Find the next message number + auto last_msg = in_msgs.end(); + if (last_msg != in_msgs.begin()) { + --last_msg; + msg_num = last_msg->message_number + 1; + } + + while (ptr + 6 <= end) { // minimum: 2 bytes type + 4 bytes length + // Read assertion type (2 bytes, big-endian) + uint16_t atype = (uint16_t(uint8_t(ptr[0])) << 8) | uint8_t(ptr[1]); + ptr += 2; + + // Read payload length (4 bytes, big-endian) + uint32_t plen = (uint32_t(uint8_t(ptr[0])) << 24) | + (uint32_t(uint8_t(ptr[1])) << 16) | + (uint32_t(uint8_t(ptr[2])) << 8) | + uint32_t(uint8_t(ptr[3])); + ptr += 4; + + check(ptr + plen <= end, "depot: message payload exceeds buffer"); + + std::vector payload(ptr, ptr + plen); + ptr += plen; + + assertion_type_t assertion = static_cast(atype); + + // FR-106: Classify message type + bool is_challenge = (assertion == assertion_type_challenge_response); + + // FR-107: Messages requiring underwriting -> PENDING; others -> READY + // Challenge messages are always READY (FR-108) + message_status_t msg_status; + if (is_challenge) { + msg_status = message_status_ready; + } else { + // Messages that need underwriting: swaps, purchases + // For now, purchases need underwriting; balance sheets and operator + // registration do not. + switch (assertion) { + case assertion_type_wire_purchase: + case assertion_type_stake_update: + msg_status = message_status_pending; + break; + default: + msg_status = message_status_ready; + break; + } + } + + in_msgs.emplace(get_self(), [&](auto& m) { + m.message_number = msg_num++; + m.assertion_type = assertion; + m.status = msg_status; + m.payload = payload; + }); + + // FR-108: Process challenge messages immediately + if (is_challenge) { + process_assertion(msg_num - 1, assertion, payload); + } + } + + // Update inbound epoch tracking + opp_epoch_in_table epoch_tbl(get_self(), chain_scope); + auto ep_it = epoch_tbl.find(epoch_number); + if (ep_it == epoch_tbl.end()) { + epoch_tbl.emplace(get_self(), [&](auto& e) { + e.epoch_number = epoch_number; + e.start_message = msg_num > 0 ? msg_num - 1 : 0; // approximate + e.end_message = msg_num; + e.challenge_flag = (s.state == depot_state_challenge); + }); + } else { + epoch_tbl.modify(ep_it, same_payer, [&](auto& e) { + e.end_message = msg_num; + }); + } +} + +} // namespace sysio diff --git a/contracts/sysio.depot/src/opp_outbound.cpp b/contracts/sysio.depot/src/opp_outbound.cpp new file mode 100644 index 0000000..86ba947 --- /dev/null +++ b/contracts/sysio.depot/src/opp_outbound.cpp @@ -0,0 +1,120 @@ +#include + +namespace sysio { + +// ── emitchain (FR-201/202/203) ────────────────────────────────────────────── +// +// An elected batch operator builds the outbound message chain for an epoch. +// Collects all queued outbound messages, computes a merkle root, signs, +// and stores the chain for the Outpost to fetch. + +void depot::emitchain(name operator_account, uint64_t epoch_number) { + require_auth(operator_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + // Verify operator is elected for this epoch + verify_elected(operator_account, epoch_number); + + // Resolve operator id + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto op_it = by_account.find(operator_account.value); + check(op_it != by_account.end(), "depot: operator not registered"); + + // Check that outbound epoch hasn't already been emitted + opp_epoch_out_table epoch_tbl(get_self(), chain_scope); + auto ep_it = epoch_tbl.find(epoch_number); + check(ep_it == epoch_tbl.end(), "depot: outbound chain already emitted for this epoch"); + + // Collect outbound messages for this epoch + // Messages are queued via queue_outbound_message() during crank processing + opp_out_table out(get_self(), chain_scope); + + // Determine message range for this epoch + // Use previous epoch's end_message as start, or 0 if first epoch + uint64_t start_msg = 0; + if (epoch_number > 0) { + auto prev_ep = epoch_tbl.find(epoch_number - 1); + if (prev_ep != epoch_tbl.end()) { + start_msg = prev_ep->end_message; + } + } + + // FR-202: Build the outbound message chain payload + // Serialize all outbound messages into a single chain envelope + std::vector chain_payload; + uint64_t end_msg = start_msg; + auto it = out.lower_bound(start_msg); + + while (it != out.end()) { + // Append: assertion_type (2 bytes BE) + payload_len (4 bytes BE) + payload + uint16_t atype = uint16_t(it->assertion_type); + chain_payload.push_back(char(atype >> 8)); + chain_payload.push_back(char(atype & 0xFF)); + + uint32_t plen = it->payload.size(); + chain_payload.push_back(char((plen >> 24) & 0xFF)); + chain_payload.push_back(char((plen >> 16) & 0xFF)); + chain_payload.push_back(char((plen >> 8) & 0xFF)); + chain_payload.push_back(char(plen & 0xFF)); + + chain_payload.insert(chain_payload.end(), it->payload.begin(), it->payload.end()); + + end_msg = it->message_number + 1; + ++it; + } + + // Compute merkle root over the outbound messages + // For a single-element chain, the merkle root is the hash of the payload + checksum256 merkle = sha256(chain_payload.data(), chain_payload.size()); + + // FR-203: Compute epoch hash (hash of merkle_root + prev_epoch_hash) + checksum256 prev_epoch_hash = checksum256(); + if (epoch_number > 0) { + message_chains_table chains(get_self(), chain_scope); + auto by_epoch = chains.get_index<"byepochdir"_n>(); + uint128_t ek = (uint128_t(epoch_number - 1) << 8) | uint64_t(message_direction_outbound); + auto chain_it = by_epoch.lower_bound(ek); + if (chain_it != by_epoch.end() && + chain_it->epoch_number == epoch_number - 1 && + chain_it->direction == message_direction_outbound) { + prev_epoch_hash = chain_it->epoch_hash; + } + } + + // epoch_hash = H(merkle_root || prev_epoch_hash) + std::vector hash_input; + auto merkle_bytes = merkle.extract_as_byte_array(); + auto prev_bytes = prev_epoch_hash.extract_as_byte_array(); + hash_input.insert(hash_input.end(), merkle_bytes.begin(), merkle_bytes.end()); + hash_input.insert(hash_input.end(), prev_bytes.begin(), prev_bytes.end()); + checksum256 epoch_hash = sha256(hash_input.data(), hash_input.size()); + + // Store message chain record + message_chains_table chains(get_self(), chain_scope); + chains.emplace(get_self(), [&](auto& c) { + c.id = chains.available_primary_key(); + c.direction = message_direction_outbound; + c.status = chain_status_valid; // outbound is always valid + c.epoch_number = epoch_number; + c.merkle_root = merkle; + c.epoch_hash = epoch_hash; + c.prev_epoch_hash = prev_epoch_hash; + c.payload = chain_payload; + c.operator_signature = {}; // TODO: operator signs with secp256k1 key + c.operator_id = op_it->id; + c.created_at = current_time_point(); + }); + + // Store epoch tracking record + epoch_tbl.emplace(get_self(), [&](auto& e) { + e.epoch_number = epoch_number; + e.start_message = start_msg; + e.end_message = end_msg; + e.merkle_root = merkle; + }); +} + +} // namespace sysio diff --git a/contracts/sysio.depot/src/reserves.cpp b/contracts/sysio.depot/src/reserves.cpp new file mode 100644 index 0000000..1c8f322 --- /dev/null +++ b/contracts/sysio.depot/src/reserves.cpp @@ -0,0 +1,181 @@ +#include + +namespace sysio { + +// ── setreserve (FR-701) ───────────────────────────────────────────────────── +// +// Governance / admin action to set the initial reserve state for a token pair. +// Sets the total reserve balance and its $WIRE equivalent value. + +void depot::setreserve(name authority, asset reserve_total, asset wire_equivalent) { + require_auth(authority); + + // Only the contract itself (governance) can set reserves + check(authority == get_self(), "depot: only contract authority can set reserves"); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + check(reserve_total.amount >= 0, "depot: reserve_total must be non-negative"); + check(wire_equivalent.amount >= 0, "depot: wire_equivalent must be non-negative"); + + reserves_table reserves(get_self(), chain_scope); + auto it = reserves.find(reserve_total.symbol.code().raw()); + + if (it == reserves.end()) { + reserves.emplace(get_self(), [&](auto& r) { + r.reserve_total = reserve_total; + r.wire_equivalent = wire_equivalent; + }); + } else { + reserves.modify(it, same_payer, [&](auto& r) { + r.reserve_total = reserve_total; + r.wire_equivalent = wire_equivalent; + }); + } +} + +// ── updreserve (FR-701) ───────────────────────────────────────────────────── +// +// An elected batch operator updates a reserve balance by a delta amount. +// Called during message processing when assets are deposited or withdrawn. + +void depot::updreserve(name operator_account, symbol token_sym, int64_t delta) { + require_auth(operator_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + // Verify caller is elected + verify_elected(operator_account, s.current_epoch); + + reserves_table reserves(get_self(), chain_scope); + auto it = reserves.find(token_sym.code().raw()); + check(it != reserves.end(), "depot: reserve not found for token"); + + int64_t new_amount = it->reserve_total.amount + delta; + check(new_amount >= 0, "depot: reserve would go negative"); + + reserves.modify(it, same_payer, [&](auto& r) { + r.reserve_total.amount = new_amount; + }); +} + +// ── getquote (FR-900 / FR-703) ────────────────────────────────────────────── +// +// Read-only action that computes a swap quote based on current reserve state. +// Uses constant-product (x*y=k) pricing with the underwriting fee applied. + +void depot::getquote(symbol source_sym, symbol target_sym, asset amount) { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + check(amount.amount > 0, "depot: amount must be positive"); + check(amount.symbol == source_sym, "depot: amount symbol must match source_sym"); + + reserves_table reserves(get_self(), chain_scope); + + auto source_it = reserves.find(source_sym.code().raw()); + check(source_it != reserves.end(), "depot: source token reserve not found"); + check(source_it->reserve_total.amount > 0, "depot: source reserve is empty"); + + auto target_it = reserves.find(target_sym.code().raw()); + check(target_it != reserves.end(), "depot: target token reserve not found"); + check(target_it->reserve_total.amount > 0, "depot: target reserve is empty"); + + // Constant product: x * y = k + // output = (target_reserve * input) / (source_reserve + input) + // Apply fee: output = output * (10000 - UNDERWRITE_FEE_BPS) / 10000 + + int64_t source_reserve = source_it->reserve_total.amount; + int64_t target_reserve = target_it->reserve_total.amount; + int64_t input = amount.amount; + + // Use 128-bit multiplication to avoid overflow + __int128 numerator = __int128(target_reserve) * __int128(input); + __int128 denominator = __int128(source_reserve) + __int128(input); + int64_t raw_output = int64_t(numerator / denominator); + + // Apply fee + int64_t fee_adjusted = int64_t((__int128(raw_output) * (10000 - UNDERWRITE_FEE_BPS)) / 10000); + + check(fee_adjusted > 0, "depot: quote results in zero output"); + check(fee_adjusted < target_reserve, "depot: insufficient target reserve for this swap"); + + // This is a read-only action — the quote is returned via check message + // In production, this would use return_value or get_table_rows + // For now, we print the result + print("quote:", fee_adjusted, " ", target_sym); +} + +// ── oneshot (FR-1000) ─────────────────────────────────────────────────────── +// +// One-time warrant conversion. Burns a warrant token and mints the +// corresponding $WIRE amount to the beneficiary. +// This is a governance-controlled action. + +void depot::oneshot(name beneficiary, asset amount) { + require_auth(get_self()); + + check(is_account(beneficiary), "depot: beneficiary account does not exist"); + check(amount.amount > 0, "depot: amount must be positive"); + + auto s = get_state(); + + // FR-1000: Issue inline transfer from depot to beneficiary + // The depot contract must hold the $WIRE tokens to distribute + action( + permission_level{get_self(), "active"_n}, + s.token_contract, + "transfer"_n, + std::make_tuple(get_self(), beneficiary, amount, + std::string("oneshot warrant conversion")) + ).send(); +} + +// ── calculate_swap_rate (internal, FR-703) ────────────────────────────────── +// +// Computes the effective exchange rate in basis points between two tokens +// based on current reserve balances. + +uint64_t depot::calculate_swap_rate(symbol source, symbol target, asset amount) { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + reserves_table reserves(get_self(), chain_scope); + + auto source_it = reserves.find(source.code().raw()); + if (source_it == reserves.end() || source_it->reserve_total.amount == 0) return 0; + + auto target_it = reserves.find(target.code().raw()); + if (target_it == reserves.end() || target_it->reserve_total.amount == 0) return 0; + + // Rate in basis points: (target_wire_equivalent / source_wire_equivalent) * 10000 + // Using wire_equivalent for cross-token comparison + if (source_it->wire_equivalent.amount == 0) return 0; + + uint64_t rate_bps = uint64_t( + (__int128(target_it->wire_equivalent.amount) * 10000) / + __int128(source_it->wire_equivalent.amount) + ); + + return rate_bps; +} + +// ── check_rate_threshold (internal, FR-704) ───────────────────────────────── +// +// Verifies that an exchange rate is within acceptable bounds. +// Returns true if the rate is acceptable (deviation from 1:1 is within +// the fee threshold). + +bool depot::check_rate_threshold(uint64_t rate_bps, uint64_t threshold_bps) { + // Rate should be within [10000 - threshold, 10000 + threshold] basis points + // of the expected 1:1 ratio (adjusted for token decimals) + // For a 0.1% fee (10 bps), we allow rates between 9990 and 10010 + uint64_t lower = 10000 - threshold_bps * 100; // allow wider range + uint64_t upper = 10000 + threshold_bps * 100; + + return rate_bps >= lower && rate_bps <= upper; +} + +} // namespace sysio diff --git a/contracts/sysio.depot/src/sysio.depot.cpp b/contracts/sysio.depot/src/sysio.depot.cpp new file mode 100644 index 0000000..3770d8b --- /dev/null +++ b/contracts/sysio.depot/src/sysio.depot.cpp @@ -0,0 +1,545 @@ +#include + +namespace sysio { + +// ── State helpers ──────────────────────────────────────────────────────────── + +depot_global_state depot::get_state() { + depot_state_singleton state_tbl(get_self(), get_self().value); + check(state_tbl.exists(), "depot: contract not initialized"); + return state_tbl.get(); +} + +void depot::set_state(const depot_global_state& s) { + depot_state_singleton state_tbl(get_self(), get_self().value); + state_tbl.set(s, get_self()); +} + +// ── init ───────────────────────────────────────────────────────────────────── + +void depot::init(chain_kind_t chain_id, name token_contract) { + require_auth(get_self()); + + depot_state_singleton state_tbl(get_self(), get_self().value); + check(!state_tbl.exists() || !state_tbl.get().initialized, + "depot: already initialized"); + + check(chain_id != fc::crypto::chain_kind_unknown && chain_id != fc::crypto::chain_kind_wire, + "depot: chain_id must be an external chain (ethereum, solana, sui)"); + check(is_account(token_contract), "depot: token_contract account does not exist"); + + depot_global_state s; + s.state = depot_state_active; + s.chain_id = chain_id; + s.current_epoch = 0; + s.next_epoch = 1; + s.next_msg_out = 0; + s.last_crank_time = current_time_point(); + s.token_contract = token_contract; + s.initialized = true; + + state_tbl.set(s, get_self()); +} + +// ── bootstrap (testnet): seed opschedule for current_epoch ─────────────────── + +void depot::bootstrap() { + require_auth(get_self()); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + op_schedule_table sched(get_self(), chain_scope); + check(sched.find(s.current_epoch) == sched.end(), + "depot: schedule already exists for current epoch"); + + elect_operators_for_epoch(s.current_epoch); +} + +// ── crank (FR-801) ────────────────────────────────────────────────────────── + +void depot::crank(name operator_account) { + require_auth(operator_account); + + auto s = get_state(); + check(s.state != depot_state_paused, "depot: system is paused, manual intervention required"); + + // Verify the caller is an elected batch operator for the current epoch + verify_elected(operator_account, s.current_epoch); + + // Step 1: Expire underwriting locks (FR-402) + expire_underwriting_locks(); + + // Step 2: Process messages previously marked as READY (FR-801.3) + if (s.state == depot_state_active) { + process_ready_messages(); + } + + // Step 3: Elect 7 active batch operators for NEXT message chain (FR-801.2 / FR-604) + elect_operators_for_epoch(s.next_epoch); + + // Step 4: Advance epoch + s.current_epoch = s.next_epoch; + s.next_epoch = s.next_epoch + 1; + s.last_crank_time = current_time_point(); + + set_state(s); +} + +// ── process_ready_messages (internal) ──────────────────────────────────────── + +void depot::process_ready_messages() { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + opp_in_table msgs(get_self(), chain_scope); + auto by_status = msgs.get_index<"bystatus"_n>(); + auto it = by_status.lower_bound(uint64_t(message_status_ready)); + + while (it != by_status.end() && it->status == message_status_ready) { + process_assertion(it->message_number, it->assertion_type, it->payload); + it = by_status.erase(it); + } +} + +// ── process_assertion (internal) ───────────────────────────────────────────── +// +// Payload wire format (big-endian, matching uploadmsgs serialization): +// +// balance_sheet (0xAA00): +// [sym_code(8) | reserve_amount(8) | wire_equiv_amount(8) | precision(1)] repeated +// +// stake_update (0xEE00): +// [sym_code(8) | delta_amount(8, signed) | wire_account(8) | direction(1)] +// direction: 0 = deposit (add to reserve), 1 = withdrawal (subtract) +// +// yield_reward (0xEE01): +// [sym_code(8) | reward_amount(8) | beneficiary(8)] +// +// wire_purchase (0xEE02): +// [source_sym(8) | source_amount(8) | buyer_account(8)] +// +// operator_registration (0xEE03): +// [wire_account(8) | op_type(1) | action(1) | secp_key(33) | ed_key(32)] +// action: 0 = register, 1 = deregister +// +// challenge_response (0xEE04): +// [epoch_number(8) | response_type(1) | data(N)] +// response_type: 0 = no_challenge, 1 = accept, 2 = reject +// +// slash_operator (0xEE05): +// [operator_id(8) | reason_len(2) | reason(N)] + +// Helper: read a big-endian uint64 from a byte buffer +static uint64_t read_be_u64(const char* p) { + return (uint64_t(uint8_t(p[0])) << 56) | (uint64_t(uint8_t(p[1])) << 48) | + (uint64_t(uint8_t(p[2])) << 40) | (uint64_t(uint8_t(p[3])) << 32) | + (uint64_t(uint8_t(p[4])) << 24) | (uint64_t(uint8_t(p[5])) << 16) | + (uint64_t(uint8_t(p[6])) << 8) | uint64_t(uint8_t(p[7])); +} + +static int64_t read_be_i64(const char* p) { + return static_cast(read_be_u64(p)); +} + +static uint16_t read_be_u16(const char* p) { + return (uint16_t(uint8_t(p[0])) << 8) | uint16_t(uint8_t(p[1])); +} + +void depot::process_assertion(uint64_t message_number, assertion_type_t type, + const std::vector& payload) { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + switch (type) { + + // ── 0xAA00: Balance Sheet ────────────────────────────────────────── + // Full reserve snapshot from the outpost. Each entry is 25 bytes: + // sym_code(8) + reserve_amount(8) + wire_equiv_amount(8) + precision(1) + case assertion_type_balance_sheet: { + const size_t ENTRY_SIZE = 25; + check(payload.size() >= ENTRY_SIZE, "depot: balance_sheet payload too short"); + check(payload.size() % ENTRY_SIZE == 0, "depot: balance_sheet payload not aligned"); + + reserves_table reserves(get_self(), chain_scope); + const char* ptr = payload.data(); + size_t entries = payload.size() / ENTRY_SIZE; + + for (size_t i = 0; i < entries; ++i) { + uint64_t sym_raw = read_be_u64(ptr); + int64_t reserve_amt = read_be_i64(ptr + 8); + int64_t wire_amt = read_be_i64(ptr + 16); + uint8_t precision = uint8_t(ptr[24]); + ptr += ENTRY_SIZE; + + check(reserve_amt >= 0, "depot: balance_sheet reserve_amount must be non-negative"); + check(wire_amt >= 0, "depot: balance_sheet wire_equivalent must be non-negative"); + + symbol_code sc(sym_raw); + symbol sym(sc, precision); + + auto it = reserves.find(sc.raw()); + if (it == reserves.end()) { + reserves.emplace(get_self(), [&](auto& r) { + r.reserve_total = asset(reserve_amt, sym); + r.wire_equivalent = asset(wire_amt, symbol("WIRE", 4)); + }); + } else { + reserves.modify(it, same_payer, [&](auto& r) { + r.reserve_total.amount = reserve_amt; + r.wire_equivalent.amount = wire_amt; + }); + } + } + break; + } + + // ── 0xEE00: Stake Update ─────────────────────────────────────────── + // A principal deposit or withdrawal on the external chain. + // Payload: sym_code(8) + delta(8) + wire_account(8) + direction(1) = 25 bytes + case assertion_type_stake_update: { + check(payload.size() >= 25, "depot: stake_update payload too short"); + const char* ptr = payload.data(); + + uint64_t sym_raw = read_be_u64(ptr); + int64_t delta = read_be_i64(ptr + 8); + uint64_t acct_raw = read_be_u64(ptr + 16); + uint8_t direction = uint8_t(ptr[24]); + + check(delta > 0, "depot: stake_update delta must be positive"); + + symbol_code sc(sym_raw); + name wire_account(acct_raw); + + reserves_table reserves(get_self(), chain_scope); + auto it = reserves.find(sc.raw()); + check(it != reserves.end(), "depot: stake_update reserve not found for token"); + + if (direction == 0) { + // Deposit: increase reserve + reserves.modify(it, same_payer, [&](auto& r) { + r.reserve_total.amount += delta; + }); + } else { + // Withdrawal: decrease reserve + check(it->reserve_total.amount >= delta, + "depot: stake_update withdrawal exceeds reserve"); + reserves.modify(it, same_payer, [&](auto& r) { + r.reserve_total.amount -= delta; + }); + } + + // Queue outbound confirmation so the outpost knows the depot acknowledged + std::vector confirm_payload(payload.begin(), payload.end()); + queue_outbound_message(assertion_type_stake_update, confirm_payload); + break; + } + + // ── 0xEE01: Yield Reward ─────────────────────────────────────────── + // Native chain staking yield earned. Mint equivalent WIRE to beneficiary. + // Payload: sym_code(8) + reward_amount(8) + beneficiary(8) = 24 bytes + case assertion_type_yield_reward: { + check(payload.size() >= 24, "depot: yield_reward payload too short"); + const char* ptr = payload.data(); + + uint64_t sym_raw = read_be_u64(ptr); + int64_t reward_amount = read_be_i64(ptr + 8); + uint64_t bene_raw = read_be_u64(ptr + 16); + + check(reward_amount > 0, "depot: yield_reward amount must be positive"); + + symbol_code sc(sym_raw); + name beneficiary(bene_raw); + + // Increase the reserve by the yield amount (the outpost earned it) + reserves_table reserves(get_self(), chain_scope); + auto it = reserves.find(sc.raw()); + if (it != reserves.end()) { + reserves.modify(it, same_payer, [&](auto& r) { + r.reserve_total.amount += reward_amount; + }); + } + + // Calculate WIRE equivalent of the reward using current rate + // If wire_equivalent and reserve_total are set, compute proportional WIRE + int64_t wire_reward = 0; + if (it != reserves.end() && it->reserve_total.amount > 0 && + it->wire_equivalent.amount > 0) { + // wire_reward = reward_amount * (wire_equivalent / reserve_total) + wire_reward = int64_t( + (__int128(reward_amount) * __int128(it->wire_equivalent.amount)) / + __int128(it->reserve_total.amount) + ); + } + + // Issue WIRE to beneficiary if they have an account and amount > 0 + if (wire_reward > 0 && is_account(beneficiary)) { + asset wire_payout(wire_reward, symbol("WIRE", 4)); + action( + permission_level{get_self(), "active"_n}, + s.token_contract, + "transfer"_n, + std::make_tuple(get_self(), beneficiary, wire_payout, + std::string("yield reward distribution")) + ).send(); + } + break; + } + + // ── 0xEE02: WIRE Purchase ────────────────────────────────────────── + // User deposited external-chain tokens to buy WIRE. + // Constant-product swap: output = (wire_reserve * input) / (source_reserve + input) + // Payload: source_sym(8) + source_amount(8) + buyer_account(8) = 24 bytes + case assertion_type_wire_purchase: { + check(payload.size() >= 24, "depot: wire_purchase payload too short"); + const char* ptr = payload.data(); + + uint64_t sym_raw = read_be_u64(ptr); + int64_t source_amount = read_be_i64(ptr + 8); + uint64_t buyer_raw = read_be_u64(ptr + 16); + + check(source_amount > 0, "depot: wire_purchase amount must be positive"); + + symbol_code sc(sym_raw); + name buyer(buyer_raw); + + reserves_table reserves(get_self(), chain_scope); + auto source_it = reserves.find(sc.raw()); + check(source_it != reserves.end(), "depot: wire_purchase source reserve not found"); + check(source_it->reserve_total.amount > 0, "depot: wire_purchase source reserve empty"); + check(source_it->wire_equivalent.amount > 0, "depot: wire_purchase has no wire equivalent set"); + + // Constant-product swap against the wire_equivalent + int64_t source_reserve = source_it->reserve_total.amount; + int64_t wire_reserve = source_it->wire_equivalent.amount; + + // output = (wire_reserve * input) / (source_reserve + input) + __int128 numerator = __int128(wire_reserve) * __int128(source_amount); + __int128 denominator = __int128(source_reserve) + __int128(source_amount); + int64_t raw_output = int64_t(numerator / denominator); + + // Apply underwriting fee + int64_t wire_output = int64_t( + (__int128(raw_output) * (10000 - UNDERWRITE_FEE_BPS)) / 10000 + ); + + check(wire_output > 0, "depot: wire_purchase results in zero output"); + check(wire_output < wire_reserve, "depot: wire_purchase exceeds wire reserve"); + + // Update reserves: source goes up, wire equivalent goes down + reserves.modify(source_it, same_payer, [&](auto& r) { + r.reserve_total.amount += source_amount; + r.wire_equivalent.amount -= wire_output; + }); + + // Transfer WIRE to buyer + if (is_account(buyer)) { + asset wire_payout(wire_output, symbol("WIRE", 4)); + action( + permission_level{get_self(), "active"_n}, + s.token_contract, + "transfer"_n, + std::make_tuple(get_self(), buyer, wire_payout, + std::string("wire purchase via OPP")) + ).send(); + } + + // Queue outbound confirmation with the actual WIRE amount delivered + std::vector confirm_payload(payload.begin(), payload.end()); + // Append wire_output (8 bytes BE) so outpost knows what was delivered + for (int i = 7; i >= 0; --i) { + confirm_payload.push_back(char((wire_output >> (i * 8)) & 0xFF)); + } + queue_outbound_message(assertion_type_wire_purchase, confirm_payload); + break; + } + + // ── 0xEE03: Operator Registration ────────────────────────────────── + // An operator registered or deregistered on the outpost side. + // Payload: wire_account(8) + op_type(1) + action(1) + secp_key(33) + ed_key(32) = 75 bytes + case assertion_type_operator_registration: { + check(payload.size() >= 10, "depot: operator_registration payload too short"); + const char* ptr = payload.data(); + + uint64_t acct_raw = read_be_u64(ptr); + uint8_t op_type_v = uint8_t(ptr[8]); + uint8_t reg_action = uint8_t(ptr[9]); // 0=register, 1=deregister + + name wire_account(acct_raw); + operator_type_t op_type = static_cast(op_type_v); + + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto op_it = by_account.find(wire_account.value); + + if (reg_action == 0) { + // Register: create or reactivate operator + if (op_it == by_account.end()) { + // New registration — need keys from payload + check(payload.size() >= 75, + "depot: operator_registration register payload needs keys (75 bytes)"); + std::vector secp_key(ptr + 10, ptr + 43); + std::vector ed_key(ptr + 43, ptr + 75); + + ops.emplace(get_self(), [&](auto& o) { + o.id = ops.available_primary_key(); + o.op_type = op_type; + o.status = operator_status_warmup; + o.wire_account = wire_account; + o.secp256k1_pubkey = secp_key; + o.ed25519_pubkey = ed_key; + o.collateral = asset(0, symbol("WIRE", 4)); + o.registered_at = current_time_point(); + o.status_changed_at = current_time_point(); + }); + } else { + // Re-registration: reactivate if exited or cooldown + if (op_it->status == operator_status_exited || + op_it->status == operator_status_cooldown) { + by_account.modify(op_it, same_payer, [&](auto& o) { + o.status = operator_status_warmup; + o.status_changed_at = current_time_point(); + }); + } + } + } else if (reg_action == 1) { + // Deregister: move to cooldown + if (op_it != by_account.end() && + op_it->status != operator_status_slashed && + op_it->status != operator_status_exited) { + by_account.modify(op_it, same_payer, [&](auto& o) { + o.status = operator_status_cooldown; + o.status_changed_at = current_time_point(); + }); + } + } + break; + } + + // ── 0xEE04: Challenge Response ───────────────────────────────────── + // Inbound challenge data from the outpost (accept/reject/no_challenge). + // Payload: epoch_number(8) + response_type(1) + data(N) + // response_type: 0=no_challenge, 1=accept, 2=reject + case assertion_type_challenge_response: { + check(payload.size() >= 9, "depot: challenge_response payload too short"); + const char* ptr = payload.data(); + + uint64_t epoch_number = read_be_u64(ptr); + uint8_t response_type = uint8_t(ptr[8]); + + if (response_type == 0) { + // no_challenge: outpost confirms no issue — resume if we were in challenge + if (s.state == depot_state_challenge) { + resume_from_challenge(); + } + } else if (response_type == 1) { + // accept: outpost acknowledges the challenge, enters challenge mode + // The depot should already be in challenge state from the initial challenge() call + // No additional action needed — wait for chalresp from operators + } else if (response_type == 2) { + // reject: outpost says the challenge is invalid + // Find the active challenge for this epoch and resolve it + challenges_table chals(get_self(), chain_scope); + auto by_epoch = chals.get_index<"byepoch"_n>(); + auto chal_it = by_epoch.find(epoch_number); + if (chal_it != by_epoch.end() && + chal_it->status != challenge_status_resolved) { + by_epoch.modify(chal_it, same_payer, [&](auto& c) { + c.status = challenge_status_resolved; + }); + } + // Resume normal operations + if (s.state == depot_state_challenge) { + resume_from_challenge(); + } + } + break; + } + + // ── 0xEE05: Slash Operator ───────────────────────────────────────── + // The outpost detected operator misbehavior and requests a slash. + // Payload: operator_id(8) + reason_len(2) + reason(N) + case assertion_type_slash_operator: { + check(payload.size() >= 10, "depot: slash_operator payload too short"); + const char* ptr = payload.data(); + + uint64_t operator_id = read_be_u64(ptr); + uint16_t reason_len = read_be_u16(ptr + 8); + + // Validate reason length + check(payload.size() >= 10 + reason_len, + "depot: slash_operator reason exceeds payload"); + + known_operators_table ops(get_self(), chain_scope); + auto op_it = ops.find(operator_id); + + if (op_it != ops.end() && op_it->status != operator_status_slashed) { + ops.modify(op_it, same_payer, [&](auto& o) { + o.status = operator_status_slashed; + o.collateral.amount = 0; // DEC-006: collapse all collateral + o.status_changed_at = current_time_point(); + }); + + // Distribute slashed collateral (challenger gets 50%) + // For inbound slashes, we don't have a challenger account on the Wire side, + // so the collateral goes to the depot itself (governance can redistribute) + // distribute_slash(operator_id, get_self()); + } + + // Queue outbound acknowledgment so outpost knows slash was applied + queue_outbound_message(assertion_type_slash_operator, payload); + break; + } + + // ── Unknown assertion type ───────────────────────────────────────── + // Silently pass through unknown types. This prevents the deadlock bug + // where unrecognized Ethereum-native types (2001-3006) killed the crank. + default: { + // Unknown assertion type — log and skip. Do not abort the transaction. + break; + } + } +} + +// ── expire_underwriting_locks (internal, FR-402) ───────────────────────────── + +void depot::expire_underwriting_locks() { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + underwrite_table ledger(get_self(), chain_scope); + auto by_expiry = ledger.get_index<"byexpiry"_n>(); + uint64_t now_sec = current_time_point().sec_since_epoch(); + + auto it = by_expiry.begin(); + while (it != by_expiry.end() && it->unlock_at.sec_since_epoch() <= now_sec) { + if (it->status == underwrite_status_intent_submitted || + it->status == underwrite_status_intent_confirmed) { + by_expiry.modify(it, same_payer, [&](auto& e) { + e.status = underwrite_status_expired; + }); + } + ++it; + } +} + +// ── queue_outbound_message (internal) ──────────────────────────────────────── + +void depot::queue_outbound_message(assertion_type_t type, const std::vector& payload) { + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + opp_out_table out(get_self(), chain_scope); + uint64_t next_num = s.next_msg_out; + + out.emplace(get_self(), [&](auto& m) { + m.message_number = next_num; + m.assertion_type = type; + m.payload = payload; + }); + + s.next_msg_out = next_num + 1; + set_state(s); +} + +} // namespace sysio diff --git a/contracts/sysio.depot/src/underwriting.cpp b/contracts/sysio.depot/src/underwriting.cpp new file mode 100644 index 0000000..999e78b --- /dev/null +++ b/contracts/sysio.depot/src/underwriting.cpp @@ -0,0 +1,150 @@ +#include + +namespace sysio { + +// ── uwintent (FR-403) ─────────────────────────────────────────────────────── +// +// An underwriter submits intent to underwrite a pending inbound message. +// Verifies the underwriter has sufficient collateral, calculates the +// exchange rate, checks threshold, creates a LOCK entry with 6hr expiry, +// and queues an outbound confirmation. + +void depot::uwintent(name underwriter, + uint64_t message_id, + asset source_amount, + asset target_amount, + chain_kind_t source_chain, + chain_kind_t target_chain, + std::vector source_sig, + std::vector target_sig) { + require_auth(underwriter); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + check(s.state == depot_state_active, "depot: system not in active state"); + + // Resolve underwriter operator record + known_operators_table ops(get_self(), chain_scope); + auto by_account = ops.get_index<"byaccount"_n>(); + auto op_it = by_account.find(underwriter.value); + check(op_it != by_account.end(), "depot: underwriter not registered"); + check(op_it->op_type == operator_type_underwriter, "depot: account is not an underwriter"); + check(op_it->status == operator_status_active, "depot: underwriter is not active"); + + // Verify the message exists and needs underwriting (status = PENDING) + opp_in_table msgs(get_self(), chain_scope); + auto msg_it = msgs.find(message_id); + check(msg_it != msgs.end(), "depot: message not found"); + check(msg_it->status == message_status_pending, "depot: message is not pending underwriting"); + + // FR-403: Calculate exchange rate from reserve state + uint64_t rate_bps = calculate_swap_rate(source_amount.symbol, target_amount.symbol, source_amount); + + // FR-704: Verify rate is within acceptable threshold + // The threshold prevents extreme slippage / manipulation + check(check_rate_threshold(rate_bps, UNDERWRITE_FEE_BPS), + "depot: exchange rate exceeds acceptable threshold"); + + // Verify collateral covers the underwriting amount + check(op_it->collateral.amount >= target_amount.amount, + "depot: insufficient collateral for underwriting"); + + // FR-403: Create underwriting ledger entry with 6hr LOCK + underwrite_table ledger(get_self(), chain_scope); + + time_point_sec unlock_time = time_point_sec(current_time_point().sec_since_epoch() + INTENT_LOCK_SECONDS); + + ledger.emplace(get_self(), [&](auto& e) { + e.id = ledger.available_primary_key(); + e.operator_id = op_it->id; + e.status = underwrite_status_intent_submitted; + e.source_amount = source_amount; + e.target_amount = target_amount; + e.source_chain = source_chain; + e.target_chain = target_chain; + e.exchange_rate_bps = rate_bps; + e.unlock_at = unlock_time; + e.created_at = current_time_point(); + e.source_tx_hash = checksum256(); // populated on confirm + e.target_tx_hash = checksum256(); // populated on confirm + }); + + // FR-404: Queue outbound intent confirmation to Outpost + // The Outpost will use this to lock source funds + std::vector intent_payload; + // Serialize: message_id + underwriter_id + source_amount + target_amount + rate + // TODO: Proper OPP serialization format + queue_outbound_message(assertion_type_wire_purchase, intent_payload); +} + +// ── uwconfirm (FR-404) ───────────────────────────────────────────────────── +// +// Elected batch operator confirms an underwriting intent. +// Transitions from INTENT_SUBMITTED to INTENT_CONFIRMED, extends +// the lock to 24 hours, and marks the inbound message as READY. + +void depot::uwconfirm(name operator_account, uint64_t ledger_entry_id) { + require_auth(operator_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + check(s.state == depot_state_active, "depot: system not in active state"); + + // Verify caller is an elected batch operator + verify_elected(operator_account, s.current_epoch); + + // Find ledger entry + underwrite_table ledger(get_self(), chain_scope); + auto entry = ledger.find(ledger_entry_id); + check(entry != ledger.end(), "depot: underwriting entry not found"); + check(entry->status == underwrite_status_intent_submitted, + "depot: entry must be in INTENT_SUBMITTED status to confirm"); + + // Verify lock hasn't expired + check(entry->unlock_at > current_time_point(), + "depot: underwriting intent has expired"); + + // FR-404: Transition to CONFIRMED, extend lock to 24 hours + time_point_sec new_unlock = time_point_sec(current_time_point().sec_since_epoch() + CONFIRMED_LOCK_SECONDS); + + ledger.modify(entry, same_payer, [&](auto& e) { + e.status = underwrite_status_intent_confirmed; + e.unlock_at = new_unlock; + }); +} + +// ── uwcancel ──────────────────────────────────────────────────────────────── + +void depot::uwcancel(name operator_account, uint64_t ledger_entry_id, std::string reason) { + require_auth(operator_account); + + auto s = get_state(); + uint64_t chain_scope = uint64_t(s.chain_id); + + // Verify caller is an elected batch operator + verify_elected(operator_account, s.current_epoch); + + underwrite_table ledger(get_self(), chain_scope); + auto entry = ledger.find(ledger_entry_id); + check(entry != ledger.end(), "depot: underwriting entry not found"); + check(entry->status == underwrite_status_intent_submitted || + entry->status == underwrite_status_intent_confirmed, + "depot: entry cannot be cancelled in current status"); + + ledger.modify(entry, same_payer, [&](auto& e) { + e.status = underwrite_status_cancelled; + }); +} + +// ── uwexpire (FR-402) ─────────────────────────────────────────────────────── +// +// Permissionless cleanup of expired underwriting locks. +// Anyone can call this to release expired entries. + +void depot::uwexpire() { + expire_underwriting_locks(); +} + +} // namespace sysio