From 69ea51e725542175f9722d01b5ba68ecac7bd331 Mon Sep 17 00:00:00 2001 From: dtaghavi Date: Tue, 16 Dec 2025 21:49:56 +0000 Subject: [PATCH 1/4] Emissions node owner distribution added - On registration of node owners via sysio.roa inline action is called adding them to distribution table - When a node owner claims part of their distribution an inline action is made to sysio.token transferring WIRE - Initial timestamp of node owner distribution is set via setinittime and stored in singleton - Read only action returning useful struct related to a node owners distribution --- contracts/sysio.roa/sysio.roa.cpp | 15 +- contracts/sysio.system/CMakeLists.txt | 3 +- .../include/sysio.system/emissions.hpp | 98 +++ .../include/sysio.system/sysio.system.hpp | 36 +- .../include/sysio.system/uint256.hpp | 121 +++ contracts/sysio.system/src/emissions.cpp | 210 +++++ contracts/tests/emissions_tests.cpp | 830 ++++++++++++++++++ contracts/tests/sysio.roa_tests.cpp | 9 + 8 files changed, 1314 insertions(+), 8 deletions(-) create mode 100644 contracts/sysio.system/include/sysio.system/emissions.hpp create mode 100644 contracts/sysio.system/include/sysio.system/uint256.hpp create mode 100644 contracts/sysio.system/src/emissions.cpp create mode 100644 contracts/tests/emissions_tests.cpp diff --git a/contracts/sysio.roa/sysio.roa.cpp b/contracts/sysio.roa/sysio.roa.cpp index f41d269977..1f0d6aaa27 100644 --- a/contracts/sysio.roa/sysio.roa.cpp +++ b/contracts/sysio.roa/sysio.roa.cpp @@ -523,16 +523,11 @@ namespace sysio { check(status == 3 || status == 4, "Invalid status: Can only confirm (2) or reject (3)"); if(status == 2){ - regnodeowner(owner,nodereg_itr->tier); nodereg.modify(nodereg_itr,get_self(),[&](auto &row){ row.status = 2; }); - - //TODO -> Add require_receipient(account_name) call to notify council contract - // require_receipient("Council"); - } else { nodereg.modify(nodereg_itr,get_self(),[&](auto &row){ row.status = 3; @@ -643,7 +638,15 @@ namespace sysio { row.network_gen = state.network_gen; }); - // TODO: Notify Council contract if needed + // Add user to sysio.system node_owner_dist table + action( + {get_self(), "active"_n}, // auth used for this call + "sysio"_n, // contract account + "addnodeowner"_n, // action name + std::make_tuple(owner, tier) // action data + ).send(); + + // TODO: Inline action to Council contract if needed }; diff --git a/contracts/sysio.system/CMakeLists.txt b/contracts/sysio.system/CMakeLists.txt index 278f971e5e..19d6550f40 100644 --- a/contracts/sysio.system/CMakeLists.txt +++ b/contracts/sysio.system/CMakeLists.txt @@ -12,7 +12,8 @@ if(BUILD_SYSTEM_CONTRACTS) ${CMAKE_CURRENT_SOURCE_DIR}/src/limit_auth_changes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/finalizer_key.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/peer_keys.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/block_info.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/src/block_info.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/emissions.cpp) target_include_directories(sysio.system PUBLIC diff --git a/contracts/sysio.system/include/sysio.system/emissions.hpp b/contracts/sysio.system/include/sysio.system/emissions.hpp new file mode 100644 index 0000000000..fda0becf59 --- /dev/null +++ b/contracts/sysio.system/include/sysio.system/emissions.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace sysiosystem::emissions { + +/** + * Singleton table to manage emissions variables. + */ +struct [[sysio::table, sysio::contract("sysio.system")]] emission_state { + sysio::time_point_sec node_rewards_start; + + SYSLIB_SERIALIZE(emission_state, (node_rewards_start)) +}; + +typedef sysio::singleton<"emissionmngr"_n, emission_state> emissionstate_t; + +/** + * The Node Owner Distribution table tracks all claimed / unclaimed $WIRE of Node Owners. + * + * Each Node Owner is allocated a certain amount of $WIRE based on their Tier. + * Each Tier 1 - 3 has a different distribution schedule, where starting time of distribution + * is a static value set on boot of Wire Network. + */ +struct [[sysio::table, sysio::contract("sysio.system")]] node_owner_distribution +{ + sysio::name account_name; + sysio::asset total_allocation; + sysio::asset claimed; + uint32_t total_duration; + + uint64_t primary_key() const { return account_name.value; } +}; + +typedef sysio::multi_index<"nodedist"_n, node_owner_distribution> nodedist_t; + +/** + * Manages the liq staking meta data for each chain. + * + * @param chain The name of the chain this row is related to. + * @param total_shares Total number of shares of all staked users on this chain. + * @param index Index multiplier used to determine how much each share is worth at any given time. + */ +struct [[sysio::table, sysio::contract("sysio.system")]] stake_manager +{ + sysio::name chain; + wns::uint256_t total_shares; + wns::uint256_t index; + + uint64_t primary_key() const { return chain.value; } +}; + +typedef sysio::multi_index<"stakemanager"_n, stake_manager> stakemanager_t; + +/** + * TODO: Revisit shares / principal data types. + * account_name might actually need to be 'external address', to support historical warrant purchases as account_name isn't known at that time. + * Would need to look up against sysio.auth 'links' table. + * + * Scoped to chain name: eth, sol, etc.. + * + * The liq_stake table is tracking all users liquid staked shares per chain, along with their principal. + */ +struct [[sysio::table, sysio::contract("sysio.system")]] liq_stake +{ + sysio::name account_name; // TODO: Might need to be external_address + wns::uint256_t shares; + wns::uint256_t principal; + + uint64_t primary_key() const { return account_name.value; } +}; + +typedef sysio::multi_index<"liqstake"_n, liq_stake> liqstake_t; + +struct node_claim_result { + sysio::asset total_allocation; + sysio::asset claimed; + sysio::asset claimable; + bool can_claim; + + SYSLIB_SERIALIZE(node_claim_result, + (total_allocation) + (claimed) + (claimable) + (can_claim) + ) +}; + +// wns::uint256_t shareToToken(wns::uint256_t shares); + +// wns::uint256_t tokenToShare(wns::uint256_t liqAmt); + +} diff --git a/contracts/sysio.system/include/sysio.system/sysio.system.hpp b/contracts/sysio.system/include/sysio.system/sysio.system.hpp index 7bd8c77a1d..34eed2ef93 100644 --- a/contracts/sysio.system/include/sysio.system/sysio.system.hpp +++ b/contracts/sysio.system/include/sysio.system/sysio.system.hpp @@ -8,7 +8,7 @@ #include #include #include - +#include #include #include @@ -505,6 +505,40 @@ namespace sysiosystem { [[sysio::on_notify("auth.msg::onlinkauth")]] void onlinkauth(const name &user, const name &permission, const sysio::public_key &pub_key); + /** + * Sets the starting time for Node Owner distributions + * + * @param no_reward_init_time The starting timestamp + */ + [[sysio::action]] + void setinittime(const sysio::time_point_sec &no_reward_init_time); + + /** + * Called inline by sysio.roa when a Node Owner is registered adding them to the distribution table. + * + * @param account_name Account name of the registered Node Owner + * @param tier The tier of node owner they are: 1, 2, or 3. + */ + [[sysio::action]] + void addnodeowner(const sysio::name &account_name, const uint8_t &tier); + + /** + * Claim vested Node Owner distribution + * + * @param account_name Account name of Node Owner trying to claim + */ + [[sysio::action]] + void claimnodedis(const sysio::name &account_name); + + /** + * Read-only action to view claimable Node Owner distributions. + * + * @param account_name Account name of the user whose rewards you want to view. + * @return (total_allocation)(claimed)(claimable) + */ + [[sysio::action]] + emissions::node_claim_result viewnodedist(const sysio::name &account_name); + using init_action = sysio::action_wrapper<"init"_n, &system_contract::init>; using setacctram_action = sysio::action_wrapper<"setacctram"_n, &system_contract::setacctram>; using setacctnet_action = sysio::action_wrapper<"setacctnet"_n, &system_contract::setacctnet>; diff --git a/contracts/sysio.system/include/sysio.system/uint256.hpp b/contracts/sysio.system/include/sysio.system/uint256.hpp new file mode 100644 index 0000000000..8dfc219b4e --- /dev/null +++ b/contracts/sysio.system/include/sysio.system/uint256.hpp @@ -0,0 +1,121 @@ +#include +#include +#include + +namespace wns { + + struct uint256_t { + uint128_t low; + uint128_t high; + + // Default constructor + uint256_t() : high(0), low(0) {} + + // Construct from uint128_t + uint256_t(uint128_t val) : high(0), low(val) {} + uint256_t(uint128_t low_val, uint128_t high_val): high(high_val), low(low_val) {} + + + // Addition with overflow check + uint256_t operator+(const uint256_t& other) const { + uint256_t result; + result.low = low + other.low; + result.high = high + other.high; + if (result.low < low) { // Check for overflow in low part + result.high++; // Carry to high part + } + return result; + } + + // Subtraction with underflow check + uint256_t operator-(const uint256_t& other) const { + uint256_t result; + result.low = low - other.low; + result.high = high - other.high; + if (result.low > low) { // Check for underflow in low part + result.high--; // Borrow from high part + } + return result; + } + + uint256_t& operator+=(const uint256_t& other) { + uint128_t new_low = low + other.low; + high += other.high; + if( new_low < low) { //Check for overflow + high++; //Carry the one + } + low = new_low; + return *this; + } + + uint256_t& operator -=(const uint256_t& other) { + uint128_t new_low = low - other.low; + high -= other.high; + if( new_low > low ) { // Check for underflow + high--; //Borrow one + } + low = new_low; + return *this; + } + + // Less than + bool operator<(const uint256_t& other) const { + if (high == other.high) { + return low < other.low; + } + return high < other.high; + } + + // Greater than + bool operator>(const uint256_t& other) const { + if (high == other.high) { + return low > other.low; + } + return high > other.high; + } + + // Equal to + bool operator==(const uint256_t& other) const { + return (high == other.high) && (low == other.low); + } + + // Less than or equal to + bool operator<=(const uint256_t& other) const { + return *this < other || *this == other; + } + + bool operator>=(const uint256_t& other) const { + return *this > other || *this == other; + } + + // Convert to hexadecimal string for easier printing and debugging + std::string to_string() const { + char buffer[65]; // 64 hex chars + null terminator + uint64_t high_high = (uint64_t)(high >> 64); + uint64_t high_low = (uint64_t)(high); + uint64_t low_high = (uint64_t)(low >> 64); + uint64_t low_low = (uint64_t)(low); + + snprintf(buffer, sizeof(buffer), + "%016llx%016llx%016llx%016llx", + high_high, high_low, low_high, low_low); + return std::string(buffer); + } + // Print method for sysio::print compatibility + void print() const { + sysio::print(to_string()); + } + + static uint256_t from_string(const std::string& str) { + uint128_t high = (uint128_t)std::stoull(str.substr(0, 16), nullptr, 16) << 64 | + (uint128_t)std::stoull(str.substr(16, 16), nullptr, 16); + uint128_t low = (uint128_t)std::stoull(str.substr(32, 16), nullptr, 16) << 64 | + (uint128_t)std::stoull(str.substr(48, 16), nullptr, 16); + + return uint256_t(low, high); + } + + // Serialization support + SYSLIB_SERIALIZE(uint256_t, (low)(high)) + }; +} \ No newline at end of file diff --git a/contracts/sysio.system/src/emissions.cpp b/contracts/sysio.system/src/emissions.cpp new file mode 100644 index 0000000000..ede0f4e941 --- /dev/null +++ b/contracts/sysio.system/src/emissions.cpp @@ -0,0 +1,210 @@ +#include +#include + +namespace sysiosystem { + + using namespace emissions; + + // - - - - LOCAL EMISSIONS CONSTANTS - - - - + + // Adjust precision here to match your core symbol + static constexpr sysio::symbol WIRE_SYMBOL = sysio::symbol("WIRE", 8); // TODO: Set precision once we know. Can we somehow pull this from sysio.token based on the actually created TOKEN/SYMBOL? + + // Minimum amount any claim action will allow. + static const sysio::asset MIN_CLAIMABLE = sysio::asset(1000000000, WIRE_SYMBOL); + + // TODO: Fix these types, used for liq staking + // static const wns::uint256_t ETH_RAY = pow(10, 27); + // static const uint64_t SOL_RAY = pow(10, 12); + + // Node Owner total_claimable amounts (in WIRE subunits) + static const sysio::asset T1_ALLOCATION(750000000000000, WIRE_SYMBOL); + static const sysio::asset T2_ALLOCATION(100000000000000, WIRE_SYMBOL); + static const sysio::asset T3_ALLOCATION(10000000000000, WIRE_SYMBOL); + + // Durations + static constexpr uint32_t SECONDS_PER_MONTH = 30u * 24u * 60u * 60u; + static constexpr uint32_t T1_DURATION = 12u * SECONDS_PER_MONTH; // 12 months + static constexpr uint32_t T2_DURATION = 24u * SECONDS_PER_MONTH; // 24 months + static constexpr uint32_t T3_DURATION = 36u * SECONDS_PER_MONTH; // 36 months; + + namespace { + using sysio::time_point; + using sysio::time_point_sec; + using sysio::asset; + + emissions::node_claim_result compute_node_claim( + const emissions::emission_state& emission, + const emissions::node_owner_distribution& row + ) { + emissions::node_claim_result info{}; + + const uint32_t start_secs = emission.node_rewards_start.sec_since_epoch(); + sysio::check(start_secs > 0, "node rewards have not started"); + + const uint32_t duration = row.total_duration; + + const int64_t total_amount = row.total_allocation.amount; + const int64_t already_claimed = row.claimed.amount; + + // Current time + const time_point now_tp = sysio::current_time_point(); + const time_point_sec now = time_point_sec{ now_tp }; + const uint32_t now_secs = now.sec_since_epoch(); + + uint32_t elapsed = 0; + if (now_secs > start_secs) { + elapsed = now_secs - start_secs; + } + + if (elapsed > duration) { + elapsed = duration; + } + + int64_t total_vested_amount = 0; + if (elapsed == 0) { + // nothing vested yet + total_vested_amount = 0; + } else if (elapsed == duration) { + // fully vested: everything is available (modulo already_claimed) + total_vested_amount = total_amount; + } else { + // partially vested: do the linear fraction with 128-bit intermediate + __int128 numerator = static_cast<__int128>(total_amount) * + static_cast<__int128>(elapsed); + + total_vested_amount = + static_cast(numerator / duration); + } + + int64_t claimable_amount = total_vested_amount - already_claimed; + if (claimable_amount < 0) { + claimable_amount = 0; + } + + info.total_allocation = row.total_allocation; + info.claimed = row.claimed; + info.claimable = asset{claimable_amount, row.total_allocation.symbol}; + info.can_claim = (claimable_amount >= MIN_CLAIMABLE.amount || elapsed == duration); + + return info; + } + + } // anonymous namespace + + // - - - - CONTRACT ACTIONS - - - - + + void system_contract::setinittime(const sysio::time_point_sec &no_reward_init_time) { + require_auth(get_self()); + + // TODO: Do we want to add any validation to init_time? + + emissionstate_t emissionstate(get_self(), get_self().value); + check(!emissionstate.exists(), "emission table already exists"); + + emissionstate.set(emission_state{ + .node_rewards_start = no_reward_init_time + }, get_self()); + } + + void system_contract::addnodeowner(const sysio::name &account_name, const uint8_t &tier) { + // TODO: Not positive this would be sent from sysio.roa's authority need to check. Idea is this would be called as an inline action. + // Called inline from sysio.roa + require_auth("sysio.roa"_n); + + // Tier sanity check + sysio::check(tier >= 1 && tier <=3, "invalid tier"); + + // Get Emission state info + emissionstate_t emission_s(get_self(), get_self().value); + sysio::check(emission_s.exists(), "emission state not initialized"); + + // Ensure this account isn't already in the table. + nodedist_t nodedist(get_self(), get_self().value); + auto itr = nodedist.find(account_name.value); + check(itr == nodedist.end(), "account already exists"); + + sysio::asset total_allocation; + uint32_t duration_seconds = 0; + + switch (tier) { + case 1: + total_allocation = T1_ALLOCATION; + duration_seconds = T1_DURATION; + break; + case 2: + total_allocation = T2_ALLOCATION; + duration_seconds = T2_DURATION; + break; + case 3: + total_allocation = T3_ALLOCATION; + duration_seconds = T3_DURATION; + break; + default: + sysio::check(false, "invalid tier"); + break; + } + + nodedist.emplace(get_self(), [&](auto& row) { + row.account_name = account_name; + row.total_allocation = total_allocation; + row.claimed = sysio::asset(0, total_allocation.symbol); + row.total_duration = duration_seconds; + }); + } + + void system_contract::claimnodedis(const sysio::name &account_name) { + // Can only claim for self + require_auth(account_name); + + // Load emission state (global start time) + emissionstate_t emission_s(get_self(), get_self().value); + sysio::check(emission_s.exists(), "emission state not initialized"); + const auto emission = emission_s.get(); + + // Lookup node owner row + nodedist_t nodedist(get_self(), get_self().value); + auto itr = nodedist.find(account_name.value); + sysio::check(itr != nodedist.end(), "account is not a node owner"); + const auto& row = *itr; + + sysio::check(row.claimed != row.total_allocation, "all node owner rewards already claimed"); + + // Get claim info. + auto info = compute_node_claim(emission, row); + sysio::check(info.can_claim, "claim amount below minimum threshold"); + + // Update internal accounting + nodedist.modify(itr, get_self(), [&](auto& mrow) { + mrow.claimed += info.claimable; + }); + + sysio::action( + {get_self(), "active"_n}, // auth used for this call + "sysio.token"_n, // contract account + "transfer"_n, // action name + std::make_tuple( + get_self(), + account_name, + info.claimable, + std::string("Node Owner distribution") + ) // action data + ).send(); + } + + emissions::node_claim_result system_contract::viewnodedist(const sysio::name &account_name) { + // Load emission state + emissionstate_t emission_s(get_self(), get_self().value); + sysio::check(emission_s.exists(), "emission state not initialized"); + const auto emission = emission_s.get(); + + // Lookup node owner distribution row + nodedist_t nodedist(get_self(), get_self().value); + auto itr = nodedist.find(account_name.value); + sysio::check(itr != nodedist.end(), "account is not a node owner"); + const auto& row = *itr; + + return compute_node_claim(emission, row); + } + +} \ No newline at end of file diff --git a/contracts/tests/emissions_tests.cpp b/contracts/tests/emissions_tests.cpp new file mode 100644 index 0000000000..8c5af4a745 --- /dev/null +++ b/contracts/tests/emissions_tests.cpp @@ -0,0 +1,830 @@ +// contracts/tests/emissions_tests.cpp +// +// Focus: emissions logic in sysio.system: +// - setinittime singleton initialization and immutability +// - addnodeowner authorization + input validation + row creation per tier +// - viewnodedist functional behavior (claimable/can_claim) across time states +// - claimnodedis authorization + gating rules + claimed accounting updates + inline token ftransfer +// - sysio.roa::forcereg wiring: inline addnodeowner occurs and writes nodedist +// +// TODO: +// - Add tests for liq_stake/stakemanager logic (once actions are built). +// - Revisit time stamp tests related to node owner dist. Check with team on type / format of these. +// - Producer pay +// - BatchOp pay +// - Challenger pay +// - Underwriter pay + +#include + +#include + +#include +#include + +#include "sysio.system_tester.hpp" + +#include +#include +#include + +using namespace sysio::testing; +using namespace sysio; +using namespace sysio::chain; +using namespace fc; +using namespace std; + +using mvo = fc::mutable_variant_object; + +// sysio.roa is the authority expected by sysio.system::addnodeowner (require_auth("sysio.roa"_n)) +static constexpr account_name ROA = "sysio.roa"_n; + +// Keep these in sync with contracts/sysio.system/src/emissions.cpp +static constexpr uint32_t SECONDS_PER_MONTH = 30u * 24u * 60u * 60u; +static constexpr uint32_t T1_DURATION = 12u * SECONDS_PER_MONTH; +static constexpr uint32_t T2_DURATION = 24u * SECONDS_PER_MONTH; +static constexpr uint32_t T3_DURATION = 36u * SECONDS_PER_MONTH; + +// MIN_CLAIMABLE in emissions.cpp: asset(1000000000, WIRE_SYMBOL) +static constexpr int64_t MIN_CLAIMABLE_AMOUNT = 1'000'000'000; + +// In unit tests we use sysio::chain::* types; chain::symbol is not constexpr. +static const symbol WIRE_SYMBOL = symbol(8, "WIRE"); + +// Node Owner total_claimable amounts (in WIRE subunits) +static const asset T1_ALLOCATION(750000000000000, WIRE_SYMBOL); +static const asset T2_ALLOCATION(100000000000000, WIRE_SYMBOL); +static const asset T3_ALLOCATION(10000000000000, WIRE_SYMBOL); + +static constexpr account_name TOKEN = "sysio.token"_n; +static constexpr uint8_t NETWORK_GEN = 0; + +// Keep as a string because sysio.token table helpers in tests use symbol::from_string("p,SYM") +static const std::string WIRE_SYM_STR = "8,WIRE"; + +// Fund sysio heavily so claims never fail due to insufficient token balance. +static const asset WIRE_MAX_SUPPLY = asset::from_string("1000000000.00000000 WIRE"); +static const asset WIRE_ISSUE_TO_SYSIO = asset::from_string("1000000000.00000000 WIRE"); + +// Mirror the on-chain return struct layout for viewnodedist. +// sysio.system::viewnodedist returns a packed node_claim_result. +struct node_claim_result { + asset total_allocation; + asset claimed; + asset claimable; + bool can_claim; +}; +FC_REFLECT( node_claim_result, (total_allocation)(claimed)(claimable)(can_claim) ) + +static time_point_sec tpsec(uint32_t secs) { + return time_point_sec{ secs }; +} + +// Quiet substring checker for action_result strings. +static void require_substr(const std::string& s, const std::string& needle) { + BOOST_REQUIRE( s.find(needle) != std::string::npos ); +} + +class sysio_emissions_tester : public tester { +public: + sysio_emissions_tester() { + produce_blocks(2); + + // --- sysio.system (emissions lives here) --- + set_code( config::system_account_name, test_contracts::sysio_system_wasm() ); + set_abi ( config::system_account_name, test_contracts::sysio_system_abi() ); + + base_tester::push_action( + config::system_account_name, + "init"_n, + config::system_account_name, + mvo()("version", 0) + ("core", symbol(CORE_SYMBOL).to_string()) + ); + produce_blocks(1); + + // sysio.system ABI serializer + { + const auto* accnt = control->find_account_metadata( config::system_account_name ); + BOOST_REQUIRE( accnt != nullptr ); + abi_def abi; + BOOST_REQUIRE_EQUAL( abi_serializer::to_abi(accnt->abi, abi), true ); + sysio_abi_ser.set_abi( abi, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + + // --- sysio.roa is expected to already be deployed by the harness --- + { + const auto* accnt = control->find_account_metadata( ROA ); + BOOST_REQUIRE( accnt != nullptr ); + abi_def abi; + BOOST_REQUIRE_EQUAL( abi_serializer::to_abi(accnt->abi, abi), true ); + roa_abi_ser.set_abi( abi, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + + // --- sysio.token setup (only create the account if it doesn't exist) --- + if (!control->db().find(TOKEN)) { + create_accounts({ TOKEN }, false, false, false, true); // include_ram_gift = true + produce_blocks(1); + } + + // --- RAM policy for sysio.token --- + if (get_roa_policy(TOKEN, "nodedaddy"_n).is_null()) { + auto tr = addpolicy_ram_only( "nodedaddy"_n, TOKEN, asset::from_string("500.0000 SYS") ); + BOOST_REQUIRE( tr ); + BOOST_REQUIRE( !tr->except ); + produce_blocks(1); + } + + set_code( TOKEN, contracts::token_wasm() ); + set_abi ( TOKEN, contracts::token_abi().data() ); + set_privileged( TOKEN ); + produce_blocks(1); + + // sysio.token ABI serializer + { + const auto* accnt = control->find_account_metadata( TOKEN ); + BOOST_REQUIRE( accnt != nullptr ); + abi_def abi; + BOOST_REQUIRE_EQUAL( abi_serializer::to_abi(accnt->abi, abi), true ); + token_abi_ser.set_abi( abi, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + + // --- Ensure WIRE exists + fund sysio for claim transfers --- + if (get_token_stats(WIRE_SYM_STR).is_null()) { + BOOST_REQUIRE_EQUAL( success(), token_create(config::system_account_name, WIRE_MAX_SUPPLY) ); + produce_blocks(1); + } + + if (get_wire_balance(config::system_account_name) < WIRE_ISSUE_TO_SYSIO) { + BOOST_REQUIRE_EQUAL( + success(), + token_issue_to_self( + config::system_account_name, + WIRE_ISSUE_TO_SYSIO, + "fund sysio for emissions claim tests" + ) + ); + produce_blocks(1); + } + } + + + /// Current head block time in seconds since epoch (used for deterministic time tests). + uint32_t head_secs() const { + return time_point_sec(control->head().block_time()).sec_since_epoch(); + } + + /// IMPORTANT: + /// Create accounts using the same behavior as the other suites in this repo + /// The last bool parameter `include_ram_gift=true` ensures accounts have RAM, + void create_user_accounts( std::initializer_list accts ) { + vector v(accts.begin(), accts.end()); + create_accounts( v, false, false, false, true ); + produce_blocks(1); + } + + // ----------------------------- + // sysio.system action helpers + // ----------------------------- + + action_result setinittime( account_name signer, time_point_sec start ) { + return push_system_action( + signer, + "setinittime"_n, + mvo()("no_reward_init_time", start) + ); + } + + action_result addnodeowner( account_name signer, account_name owner, uint8_t tier ) { + return push_system_action( + signer, + "addnodeowner"_n, + mvo()("account_name", owner) + ("tier", tier) + ); + } + + action_result claimnodedis( account_name signer, account_name owner ) { + return push_system_action( + signer, + "claimnodedis"_n, + mvo()("account_name", owner) + ); + } + + // ----------------------------- + // viewnodedist return decoding + // ----------------------------- + + /// Calls sysio.system::viewnodedist and decodes the return_value into node_claim_result. + /// We search action_traces for the sysio.system receiver trace for this action to avoid + /// decoding the wrong trace in a nested/inline scenario. + node_claim_result viewnodedist( account_name signer, account_name owner ) { + auto trace = push_system_action_trace( + signer, + "viewnodedist"_n, + mvo()("account_name", owner) + ); + + BOOST_REQUIRE(trace); + if (trace->except) { + BOOST_FAIL( trace->except->to_detail_string() ); + } + BOOST_REQUIRE(!trace->action_traces.empty()); + + const action_trace* found = nullptr; + for (const auto& at : trace->action_traces) { + if (at.receiver == config::system_account_name && at.act.name == "viewnodedist"_n) { + found = &at; + break; + } + } + + BOOST_REQUIRE(found != nullptr); + BOOST_REQUIRE(!found->return_value.empty()); + + return fc::raw::unpack( found->return_value ); + } + + // ----------------------------- + // sysio.roa wiring (forcereg) + // ----------------------------- + + /// Executes sysio.roa::forcereg and returns a trace so we can assert that + /// an inline sysio.system::addnodeowner occurred. + transaction_trace_ptr forcereg_trace( account_name signer, account_name owner, uint8_t tier ) { + return push_roa_action_trace( + signer, + "forcereg"_n, + mvo()("owner", owner) + ("tier", tier) + ); + } + + // ----------------------------- + // Table readers (ABI decoding) + // ----------------------------- + + fc::variant get_emission_state() { + auto data0 = get_row_by_account(config::system_account_name, + config::system_account_name, + "emissionmngr"_n, + account_name{0}); + if (!data0.empty()) { + return sysio_abi_ser.binary_to_variant("emission_state", data0, + abi_serializer::create_yield_function(abi_serializer_max_time)); + } + + vector data1 = get_row_by_account( + config::system_account_name, + config::system_account_name, + "emissionmngr"_n, + "emissionmngr"_n + ); + if (!data1.empty()) { + return sysio_abi_ser.binary_to_variant( + "emission_state", + data1, + abi_serializer::create_yield_function(abi_serializer_max_time) + ); + } + + return fc::variant(); + } + + /// Reads a row from the node owner distribution table: + /// typedef sysio::multi_index<"nodedist"_n, node_owner_distribution> nodedist_t; + fc::variant get_nodedist_row( account_name owner ) { + vector data = get_row_by_account( + config::system_account_name, + config::system_account_name, + "nodedist"_n, + owner + ); + + return data.empty() + ? fc::variant() + : sysio_abi_ser.binary_to_variant( + "node_owner_distribution", + data, + abi_serializer::create_yield_function(abi_serializer_max_time) + ); + } + + asset get_wire_balance( account_name acc ) { + auto row = get_token_account_row(acc, WIRE_SYM_STR); + if (row.is_null()) + return asset(0, WIRE_SYMBOL); + return row["balance"].as(); + } + + fc::variant get_token_stats( const std::string& symbolname ) { + auto symb = sysio::chain::symbol::from_string(symbolname); + auto symbol_code = symb.to_symbol_code().value; + + std::vector data = get_row_by_account( + TOKEN, + name(symbol_code), // scope = symbol_code + "stat"_n, + account_name(symbol_code) + ); + + return data.empty() + ? fc::variant() + : token_abi_ser.binary_to_variant( + "currency_stats", + data, + abi_serializer::create_yield_function(abi_serializer_max_time) + ); + } + + fc::variant get_token_account_row( account_name acc, const std::string& symbolname ) { + auto symb = sysio::chain::symbol::from_string(symbolname); + auto symbol_code = symb.to_symbol_code().value; + + std::vector data = get_row_by_account( + TOKEN, + acc, + "accounts"_n, + account_name(symbol_code) + ); + + return data.empty() + ? fc::variant() + : token_abi_ser.binary_to_variant( + "account", + data, + abi_serializer::create_yield_function(abi_serializer_max_time) + ); + } + + transaction_trace_ptr addpolicy_ram_only( account_name issuer, account_name owner, asset ram_weight ) { + // NOTE: owner == sysio.token (sysio.*), so NET/CPU MUST be zero per ROA rules. + return base_tester::push_action( + ROA, + "addpolicy"_n, + vector{{ issuer, "active"_n }}, + mvo() + ("owner", owner) + ("issuer", issuer) + ("net_weight", asset::from_string("0.0000 SYS")) + ("cpu_weight", asset::from_string("0.0000 SYS")) + ("ram_weight", ram_weight) + ("time_block", control->head().block_num()) + ("network_gen", NETWORK_GEN) + ); + } + +private: + // ----------------------------- + // Internal push helpers + // ----------------------------- + + action_result push_system_action( const account_name& signer, + const action_name& name, + const variant_object& data ) { + const string action_type_name = sysio_abi_ser.get_action_type(name); + + action act; + act.account = config::system_account_name; + act.name = name; + act.data = sysio_abi_ser.variant_to_binary( + action_type_name, + data, + abi_serializer::create_yield_function(abi_serializer_max_time) + ); + + return base_tester::push_action( std::move(act), signer.to_uint64_t() ); + } + + transaction_trace_ptr push_system_action_trace( const account_name& signer, + const action_name& name, + const variant_object& data ) { + return base_tester::push_action( + config::system_account_name, + name, + vector{{ signer, "active"_n }}, + data + ); + } + + transaction_trace_ptr push_roa_action_trace( const account_name& signer, + const action_name& name, + const variant_object& data ) { + return base_tester::push_action( + ROA, + name, + vector{{ signer, "active"_n }}, + data + ); + } + + action_result push_token_action( const account_name& signer, + const action_name& name, + const variant_object& data ) { + const std::string action_type_name = token_abi_ser.get_action_type(name); + + action act; + act.account = TOKEN; + act.name = name; + act.data = token_abi_ser.variant_to_binary( + action_type_name, data, + abi_serializer::create_yield_function(abi_serializer_max_time) + ); + + return base_tester::push_action( std::move(act), signer.to_uint64_t() ); + } + + action_result token_create( account_name issuer, asset maximum_supply ) { + // signer is sysio.token (matches sysio.token_tests.cpp) + return push_token_action( TOKEN, "create"_n, mvo() + ("issuer", issuer) + ("maximum_supply", maximum_supply) + ); + } + + action_result token_issue_to_self( account_name issuer, asset quantity, const std::string& memo ) { + // signer is issuer, and token contract enforces "to == issuer" (matches sysio.token_tests.cpp) + return push_token_action( issuer, "issue"_n, mvo() + ("to", issuer) + ("quantity", quantity) + ("memo", memo) + ); + } + + fc::variant get_roa_policy( account_name policy_owner, account_name issuer ) { + // policies table is scoped by issuer; primary key is policy_owner + // This is exactly how sysio.roa_tests.cpp does it. :contentReference[oaicite:2]{index=2} + const auto& db = control->db(); + if (const auto* table = db.find( + boost::make_tuple(ROA, issuer, "policies"_n))) { + if (auto* obj = db.find( + boost::make_tuple(table->id, policy_owner.to_uint64_t()))) { + const vector data(obj->value.data(), obj->value.data() + obj->value.size()); + if (!data.empty()) { + return roa_abi_ser.binary_to_variant( + "policies", data, + abi_serializer::create_yield_function(abi_serializer_max_time) + ); + } + } + } + return fc::variant(); + } + +private: + abi_serializer sysio_abi_ser; + abi_serializer roa_abi_ser; + abi_serializer token_abi_ser; +}; + +BOOST_AUTO_TEST_SUITE(sysio_emissions_tests) + +// ----------------------------------------------------------------------------- +// setinittime +// ----------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE( setinittime_requires_sysio_auth, sysio_emissions_tester ) try { + // setinittime requires sysio.system's authority (require_auth(get_self())) + create_user_accounts({ "alice"_n }); + + auto r = setinittime( "alice"_n, tpsec(head_secs()) ); + BOOST_REQUIRE( r != success() ); + require_substr( r, "missing authority of sysio" ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( setinittime_singleton_write_and_reprotect, sysio_emissions_tester ) try { + // First call should initialize the singleton, second call should be blocked. + auto before = get_emission_state(); + BOOST_REQUIRE( before.is_null() ); + + const uint32_t start = head_secs(); // must be > 0 to pass compute checks elsewhere + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(start) ) ); + + auto after = get_emission_state(); + BOOST_REQUIRE( !after.is_null() ); + BOOST_REQUIRE( after.is_object() ); + BOOST_REQUIRE_EQUAL( after["node_rewards_start"].as().sec_since_epoch(), start ); + + auto r = setinittime( config::system_account_name, tpsec(start) ); + BOOST_REQUIRE( r != success() ); + require_substr( r, "emission table already exists" ); +} FC_LOG_AND_RETHROW() + +// ----------------------------------------------------------------------------- +// addnodeowner +// ----------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE( addnodeowner_requires_emission_initialized, sysio_emissions_tester ) try { + create_user_accounts({ "nodeowner1"_n }); + + auto r = addnodeowner( ROA, "nodeowner1"_n, 1 ); + BOOST_REQUIRE( r != success() ); + require_substr( r, "emission state not initialized" ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( addnodeowner_requires_sysio_roa_auth, sysio_emissions_tester ) try { + create_user_accounts({ "alice"_n, "nodeowner2"_n }); + + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(head_secs()) ) ); + + auto r = addnodeowner( "alice"_n, "nodeowner2"_n, 1 ); + BOOST_REQUIRE( r != success() ); + require_substr( r, "missing authority of sysio.roa" ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( addnodeowner_rejects_invalid_tier, sysio_emissions_tester ) try { + // Tier must be 1..3 + create_user_accounts({ "nodeowner3"_n }); + + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(head_secs()) ) ); + + auto r1 = addnodeowner( ROA, "nodeowner3"_n, 0 ); + BOOST_REQUIRE( r1 != success() ); + require_substr( r1, "invalid tier" ); + + auto r2 = addnodeowner( ROA, "nodeowner3"_n, 4 ); + BOOST_REQUIRE( r2 != success() ); + require_substr( r2, "invalid tier" ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( addnodeowner_writes_expected_rows_for_each_tier, sysio_emissions_tester ) try { + // Valid tiers should create a row in nodedist with expected allocations/durations. + // Also verifies uniqueness constraint (account already exists). + create_user_accounts({ "t1"_n, "t2"_n, "t3"_n }); + + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(head_secs()) ) ); + + BOOST_REQUIRE_EQUAL( success(), addnodeowner( ROA, "t1"_n, 1 ) ); + auto r1 = get_nodedist_row("t1"_n); + BOOST_REQUIRE( !r1.is_null() ); + BOOST_REQUIRE_EQUAL( r1["account_name"].as(), "t1"_n ); + BOOST_REQUIRE_EQUAL( r1["total_allocation"].as(), T1_ALLOCATION ); + BOOST_REQUIRE_EQUAL( r1["claimed"].as(), asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE_EQUAL( r1["total_duration"].as(), T1_DURATION ); + + BOOST_REQUIRE_EQUAL( success(), addnodeowner( ROA, "t2"_n, 2 ) ); + auto r2 = get_nodedist_row("t2"_n); + BOOST_REQUIRE( !r2.is_null() ); + BOOST_REQUIRE_EQUAL( r2["total_allocation"].as(), T2_ALLOCATION ); + BOOST_REQUIRE_EQUAL( r2["claimed"].as(), asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE_EQUAL( r2["total_duration"].as(), T2_DURATION ); + + BOOST_REQUIRE_EQUAL( success(), addnodeowner( ROA, "t3"_n, 3 ) ); + auto r3 = get_nodedist_row("t3"_n); + BOOST_REQUIRE( !r3.is_null() ); + BOOST_REQUIRE_EQUAL( r3["total_allocation"].as(), T3_ALLOCATION ); + BOOST_REQUIRE_EQUAL( r3["claimed"].as(), asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE_EQUAL( r3["total_duration"].as(), T3_DURATION ); + + auto dup = addnodeowner( ROA, "t3"_n, 3 ); + BOOST_REQUIRE( dup != success() ); + require_substr( dup, "account already exists" ); +} FC_LOG_AND_RETHROW() + +// ----------------------------------------------------------------------------- +// viewnodedist / claimnodedis +// ----------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE( no_vesting_yet_start_in_future_blocks_claim, sysio_emissions_tester ) try { + // If start time is in the future, elapsed==0 => claimable==0 and can_claim==false. + create_user_accounts({ "nodefuture"_n }); + + const uint32_t start_future = head_secs() + 10'000; + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(start_future) ) ); + BOOST_REQUIRE_EQUAL( success(), addnodeowner( ROA, "nodefuture"_n, 1 ) ); + + auto info = viewnodedist( "nodefuture"_n, "nodefuture"_n ); + BOOST_REQUIRE_EQUAL(info.total_allocation, T1_ALLOCATION); + BOOST_REQUIRE_EQUAL( info.claimed, asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE_EQUAL( info.claimable, asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE( !info.can_claim ); + + const asset sys_before = get_wire_balance(config::system_account_name); + const asset user_before = get_wire_balance("nodefuture"_n); + + auto r = claimnodedis( "nodefuture"_n, "nodefuture"_n ); + BOOST_REQUIRE( r != success() ); + require_substr( r, "claim amount below minimum threshold" ); + + BOOST_REQUIRE_EQUAL( get_wire_balance(config::system_account_name), sys_before ); + BOOST_REQUIRE_EQUAL( get_wire_balance("nodefuture"_n), user_before ); + + // Ensure claim did not mutate table + auto row = get_nodedist_row("nodefuture"_n); + BOOST_REQUIRE_EQUAL( row["claimed"].as(), asset(0, WIRE_SYMBOL) ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( mid_vesting_claimable_grows_but_gate_blocks_until_min_threshold, sysio_emissions_tester ) try { + create_user_accounts({ "nodemid"_n }); + + // Use Tier 3 so MIN_CLAIMABLE is not reached quickly. + // Only ~60s vested => claimable > 0 but still < MIN. + const uint32_t now = head_secs(); + const uint32_t start = now - 60; + + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(start) ) ); + BOOST_REQUIRE_EQUAL( success(), addnodeowner( ROA, "nodemid"_n, 3 ) ); + + auto info1 = viewnodedist( "nodemid"_n, "nodemid"_n ); + BOOST_REQUIRE_EQUAL(info1.total_allocation, T3_ALLOCATION); + BOOST_REQUIRE_EQUAL( info1.claimed, asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE( info1.claimable.get_amount() > 0 ); + BOOST_REQUIRE( info1.claimable.get_amount() < MIN_CLAIMABLE_AMOUNT ); + BOOST_REQUIRE( !info1.can_claim ); + + // Move time forward a bit; claimable should increase but still stay below MIN. + produce_blocks(200); + + auto info2 = viewnodedist( "nodemid"_n, "nodemid"_n ); + BOOST_REQUIRE_EQUAL( info2.claimed, asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE( info2.claimable.get_amount() > info1.claimable.get_amount() ); + BOOST_REQUIRE( info2.claimable.get_amount() < MIN_CLAIMABLE_AMOUNT ); + BOOST_REQUIRE( !info2.can_claim ); + + auto r = claimnodedis( "nodemid"_n, "nodemid"_n ); + BOOST_REQUIRE( r != success() ); + require_substr( r, "claim amount below minimum threshold" ); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( full_vesting_allows_claim_then_blocks_second_claim, sysio_emissions_tester ) try { + // If elapsed >= duration, compute_node_claim clamps elapsed to duration and makes all remaining claimable. + // Then claimnodedis should update claimed to equal total_allocation and block subsequent claims. + create_user_accounts({ "nodefull"_n }); + + const uint32_t start = head_secs() - (T1_DURATION + 10); + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(start) ) ); + BOOST_REQUIRE_EQUAL( success(), addnodeowner( ROA, "nodefull"_n, 1 ) ); + + auto before = viewnodedist( "nodefull"_n, "nodefull"_n ); + BOOST_REQUIRE_EQUAL(before.total_allocation, T1_ALLOCATION); + BOOST_REQUIRE_EQUAL( before.claimed, asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE_EQUAL( before.claimable, before.total_allocation ); + BOOST_REQUIRE( before.can_claim ); + + // Get initial $WIRE balance + const asset sys_before = get_wire_balance(config::system_account_name); + const asset user_before = get_wire_balance("nodefull"_n); + const asset expected = before.claimable; + + BOOST_REQUIRE_EQUAL( success(), claimnodedis( "nodefull"_n, "nodefull"_n ) ); + produce_blocks(1); + + // Ensure sysio.token transfer succeeds. + BOOST_REQUIRE_EQUAL( get_wire_balance("nodefull"_n), user_before + expected ); + BOOST_REQUIRE_EQUAL( get_wire_balance(config::system_account_name), sys_before - expected ); + + auto row = get_nodedist_row("nodefull"_n); + BOOST_REQUIRE_EQUAL( row["claimed"].as(), row["total_allocation"].as() ); + + auto after = viewnodedist( "nodefull"_n, "nodefull"_n ); + BOOST_REQUIRE_EQUAL( after.claimable, asset(0, WIRE_SYMBOL) ); + + // Snapshot of balance before expected failure to claim. + const asset sys_before2 = get_wire_balance(config::system_account_name); + const asset user_before2 = get_wire_balance("nodefull"_n); + + auto r = claimnodedis( "nodefull"_n, "nodefull"_n ); + BOOST_REQUIRE( r != success() ); + require_substr( r, "all node owner rewards already claimed" ); + + // Ensure $WIRE balance didn't change after failed claim + BOOST_REQUIRE_EQUAL( get_wire_balance("nodefull"_n), user_before2 ); + BOOST_REQUIRE_EQUAL( get_wire_balance(config::system_account_name), sys_before2 ); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( claimnodedis_requires_self_auth, sysio_emissions_tester ) try { + // claimnodedis requires_auth(account_name) + create_user_accounts({ "alice"_n, "bob"_n }); + + const uint32_t start = head_secs() - (T1_DURATION + 10); + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(start) ) ); + BOOST_REQUIRE_EQUAL( success(), addnodeowner( ROA, "alice"_n, 1 ) ); + + auto r = claimnodedis( "bob"_n, "alice"_n ); + BOOST_REQUIRE( r != success() ); + require_substr( r, "missing authority of alice" ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( final_vesting_allows_small_final_remainder_below_min_threshold, sysio_emissions_tester ) try { + create_user_accounts({ "nodesmall"_n }); + + // Make vesting almost complete, but not quite: + // elapsed = duration - 10 seconds (so first claim leaves a small remainder) + const uint32_t now = head_secs(); + const uint32_t start = now - (T1_DURATION - 10); + + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(start) ) ); + BOOST_REQUIRE_EQUAL( success(), addnodeowner( ROA, "nodesmall"_n, 1 ) ); + + // First claim: should be large and allowed (>= MIN) + auto info_pre = viewnodedist( "nodesmall"_n, "nodesmall"_n ); + BOOST_REQUIRE_EQUAL( info_pre.claimed, asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE( info_pre.can_claim ); + BOOST_REQUIRE( info_pre.claimable.get_amount() >= MIN_CLAIMABLE_AMOUNT ); + + const asset sys_before1 = get_wire_balance( config::system_account_name ); + const asset user_before1 = get_wire_balance( "nodesmall"_n ); + const asset claimed1 = info_pre.claimable; + + BOOST_REQUIRE_EQUAL( success(), claimnodedis( "nodesmall"_n, "nodesmall"_n ) ); + produce_blocks(1); + + auto row_after_first = get_nodedist_row( "nodesmall"_n ); + BOOST_REQUIRE( !row_after_first.is_null() ); + + const auto claimed_after_first = row_after_first["claimed"].as(); + const auto total_after_first = row_after_first["total_allocation"].as(); + + BOOST_REQUIRE( claimed_after_first > asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE( claimed_after_first < total_after_first ); + + BOOST_REQUIRE_EQUAL( get_wire_balance("nodesmall"_n), user_before1 + claimed1 ); + BOOST_REQUIRE_EQUAL( get_wire_balance(config::system_account_name), sys_before1 - claimed1 ); + + // Advance past the remaining ~10 seconds to reach full vesting. + // (Assuming 0.5s block interval; 25 blocks ~ 12.5s) + produce_blocks(25); + + // Final remainder: should be >0 but < MIN, and allowed because elapsed == duration. + auto info_final = viewnodedist( "nodesmall"_n, "nodesmall"_n ); + BOOST_REQUIRE( info_final.can_claim ); + BOOST_REQUIRE( info_final.claimable.get_amount() > 0 ); + BOOST_REQUIRE( info_final.claimable.get_amount() < MIN_CLAIMABLE_AMOUNT ); + + const asset sys_before2 = get_wire_balance( config::system_account_name ); + const asset user_before2 = get_wire_balance( "nodesmall"_n ); + const asset remainder = info_final.claimable; + + BOOST_REQUIRE_EQUAL( success(), claimnodedis( "nodesmall"_n, "nodesmall"_n ) ); + produce_blocks(1); + + BOOST_REQUIRE_EQUAL( get_wire_balance("nodesmall"_n), user_before2 + remainder ); + BOOST_REQUIRE_EQUAL( get_wire_balance(config::system_account_name), sys_before2 - remainder ); + + // Table shows fully claimed. + auto row = get_nodedist_row( "nodesmall"_n ); + BOOST_REQUIRE( !row.is_null() ); + BOOST_REQUIRE_EQUAL( row["claimed"].as(), row["total_allocation"].as() ); + +} FC_LOG_AND_RETHROW() + +// ----------------------------------------------------------------------------- +// sysio.roa::forcereg wiring -> inline sysio.system::addnodeowner +// ----------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE( forcereg_inlines_addnodeowner_and_writes_nodedist, sysio_emissions_tester ) try { + // forcereg should inline sysio.system::addnodeowner under sysio.roa authority, + // resulting in a nodedist row for the registered owner. + create_user_accounts({ "emissinline"_n }); + + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(head_secs()) ) ); + + auto trace = forcereg_trace( ROA, "emissinline"_n, 1 ); + BOOST_REQUIRE(trace); + BOOST_REQUIRE(!trace->except); + + bool saw_inline = false; + for (const auto& at : trace->action_traces) { + if (at.receiver == config::system_account_name && + at.act.account == config::system_account_name && + at.act.name == "addnodeowner"_n) { + saw_inline = true; + break; + } + } + BOOST_REQUIRE( saw_inline ); + + auto row = get_nodedist_row("emissinline"_n); + BOOST_REQUIRE( !row.is_null() ); + BOOST_REQUIRE_EQUAL( row["account_name"].as(), "emissinline"_n ); + BOOST_REQUIRE_EQUAL( row["total_allocation"].as(), T1_ALLOCATION ); + BOOST_REQUIRE_EQUAL( row["claimed"].as(), asset(0, WIRE_SYMBOL) ); + BOOST_REQUIRE_EQUAL( row["total_duration"].as(), T1_DURATION ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( forcereg_duplicate_is_blocked_by_roa, sysio_emissions_tester ) try { + // ROA maintains its own registration table; calling forcereg twice should fail in ROA. + // We also advance blocks to avoid duplicate-trx-id issues due to identical TAPOS. + create_user_accounts({ "emissdup"_n }); + + BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(head_secs()) ) ); + + auto t1 = forcereg_trace( ROA, "emissdup"_n, 1 ); + BOOST_REQUIRE(t1); + BOOST_REQUIRE(!t1->except); + + // Avoid "Duplicate transaction" by moving TAPOS window forward. + produce_blocks(2); + + // Expected ROA error message (per sysio.roa contract). + BOOST_REQUIRE_EXCEPTION( + forcereg_trace( ROA, "emissdup"_n, 1 ), + sysio_assert_message_exception, + sysio_assert_message_is("This account is already registered.") + ); +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/contracts/tests/sysio.roa_tests.cpp b/contracts/tests/sysio.roa_tests.cpp index 6bef388f54..615a3139f6 100644 --- a/contracts/tests/sysio.roa_tests.cpp +++ b/contracts/tests/sysio.roa_tests.cpp @@ -408,6 +408,15 @@ BOOST_FIXTURE_TEST_CASE( verify_ram, sysio_roa_tester ) try { ("core", symbol(CORE_SYMBOL).to_string())); produce_block(); + // initialize emissions state so ROA inline addnodeowner can succeed + base_tester::push_action( + config::system_account_name, + "setinittime"_n, + config::system_account_name, + mvo()("no_reward_init_time", time_point_sec(control->head().block_time())) + ); + produce_blocks(1); + // roa has been activated with NODE_DADDY as a node owner // Accounts already created with ROA policy { "alice"_n, "bob"_n, "carol"_n, "darcy"_n } int64_t ram; int64_t net; int64_t cpu; From d011105ce01753e32e80bb1ea5f386901c01b00b Mon Sep 17 00:00:00 2001 From: dtaghavi Date: Wed, 17 Dec 2025 15:25:01 +0000 Subject: [PATCH 2/4] PR Cleanup - removed TODOs - Removed liq staking tables to keep PR concise --- .../include/sysio.system/emissions.hpp | 43 ------------------- contracts/sysio.system/src/emissions.cpp | 8 +--- contracts/tests/emissions_tests.cpp | 9 +--- 3 files changed, 2 insertions(+), 58 deletions(-) diff --git a/contracts/sysio.system/include/sysio.system/emissions.hpp b/contracts/sysio.system/include/sysio.system/emissions.hpp index fda0becf59..345bee68d3 100644 --- a/contracts/sysio.system/include/sysio.system/emissions.hpp +++ b/contracts/sysio.system/include/sysio.system/emissions.hpp @@ -5,7 +5,6 @@ #include #include #include -#include namespace sysiosystem::emissions { @@ -39,44 +38,6 @@ struct [[sysio::table, sysio::contract("sysio.system")]] node_owner_distribution typedef sysio::multi_index<"nodedist"_n, node_owner_distribution> nodedist_t; -/** - * Manages the liq staking meta data for each chain. - * - * @param chain The name of the chain this row is related to. - * @param total_shares Total number of shares of all staked users on this chain. - * @param index Index multiplier used to determine how much each share is worth at any given time. - */ -struct [[sysio::table, sysio::contract("sysio.system")]] stake_manager -{ - sysio::name chain; - wns::uint256_t total_shares; - wns::uint256_t index; - - uint64_t primary_key() const { return chain.value; } -}; - -typedef sysio::multi_index<"stakemanager"_n, stake_manager> stakemanager_t; - -/** - * TODO: Revisit shares / principal data types. - * account_name might actually need to be 'external address', to support historical warrant purchases as account_name isn't known at that time. - * Would need to look up against sysio.auth 'links' table. - * - * Scoped to chain name: eth, sol, etc.. - * - * The liq_stake table is tracking all users liquid staked shares per chain, along with their principal. - */ -struct [[sysio::table, sysio::contract("sysio.system")]] liq_stake -{ - sysio::name account_name; // TODO: Might need to be external_address - wns::uint256_t shares; - wns::uint256_t principal; - - uint64_t primary_key() const { return account_name.value; } -}; - -typedef sysio::multi_index<"liqstake"_n, liq_stake> liqstake_t; - struct node_claim_result { sysio::asset total_allocation; sysio::asset claimed; @@ -91,8 +52,4 @@ struct node_claim_result { ) }; -// wns::uint256_t shareToToken(wns::uint256_t shares); - -// wns::uint256_t tokenToShare(wns::uint256_t liqAmt); - } diff --git a/contracts/sysio.system/src/emissions.cpp b/contracts/sysio.system/src/emissions.cpp index ede0f4e941..f08edc75eb 100644 --- a/contracts/sysio.system/src/emissions.cpp +++ b/contracts/sysio.system/src/emissions.cpp @@ -8,15 +8,10 @@ namespace sysiosystem { // - - - - LOCAL EMISSIONS CONSTANTS - - - - // Adjust precision here to match your core symbol - static constexpr sysio::symbol WIRE_SYMBOL = sysio::symbol("WIRE", 8); // TODO: Set precision once we know. Can we somehow pull this from sysio.token based on the actually created TOKEN/SYMBOL? - + static constexpr sysio::symbol WIRE_SYMBOL = sysio::symbol("WIRE", 8); // TODO: Set precision once we know. // Minimum amount any claim action will allow. static const sysio::asset MIN_CLAIMABLE = sysio::asset(1000000000, WIRE_SYMBOL); - // TODO: Fix these types, used for liq staking - // static const wns::uint256_t ETH_RAY = pow(10, 27); - // static const uint64_t SOL_RAY = pow(10, 12); - // Node Owner total_claimable amounts (in WIRE subunits) static const sysio::asset T1_ALLOCATION(750000000000000, WIRE_SYMBOL); static const sysio::asset T2_ALLOCATION(100000000000000, WIRE_SYMBOL); @@ -108,7 +103,6 @@ namespace sysiosystem { } void system_contract::addnodeowner(const sysio::name &account_name, const uint8_t &tier) { - // TODO: Not positive this would be sent from sysio.roa's authority need to check. Idea is this would be called as an inline action. // Called inline from sysio.roa require_auth("sysio.roa"_n); diff --git a/contracts/tests/emissions_tests.cpp b/contracts/tests/emissions_tests.cpp index 8c5af4a745..ba6ae9dcfe 100644 --- a/contracts/tests/emissions_tests.cpp +++ b/contracts/tests/emissions_tests.cpp @@ -6,14 +6,7 @@ // - viewnodedist functional behavior (claimable/can_claim) across time states // - claimnodedis authorization + gating rules + claimed accounting updates + inline token ftransfer // - sysio.roa::forcereg wiring: inline addnodeowner occurs and writes nodedist -// -// TODO: -// - Add tests for liq_stake/stakemanager logic (once actions are built). -// - Revisit time stamp tests related to node owner dist. Check with team on type / format of these. -// - Producer pay -// - BatchOp pay -// - Challenger pay -// - Underwriter pay + #include From 30d6aa8b956eb6fdcff11ddd1c2d85ae325ecf0d Mon Sep 17 00:00:00 2001 From: dtaghavi Date: Wed, 17 Dec 2025 16:22:14 +0000 Subject: [PATCH 3/4] Update contract abi / wasm: roa and sysio - Updated abi / wasm for sysio.system contract with new actions / tables - Update wasm for sysio.roa to account for additional inline action in node owner registration --- contracts/sysio.roa/sysio.roa.wasm | Bin 68937 -> 69288 bytes contracts/sysio.system/sysio.system.abi | 176 ++++++++++++++++++++--- contracts/sysio.system/sysio.system.wasm | Bin 136382 -> 149430 bytes 3 files changed, 156 insertions(+), 20 deletions(-) diff --git a/contracts/sysio.roa/sysio.roa.wasm b/contracts/sysio.roa/sysio.roa.wasm index 2b223a8d726634e48cb6bab13d974cbd66d72eb3..14ecef145507fe2a39ce52d9bb8e772a9c32cd8c 100755 GIT binary patch delta 3424 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 C7HysY delta 3013 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 AOaK4? diff --git a/contracts/sysio.system/sysio.system.abi b/contracts/sysio.system/sysio.system.abi index 15ef56cc70..5e1f876596 100644 --- a/contracts/sysio.system/sysio.system.abi +++ b/contracts/sysio.system/sysio.system.abi @@ -54,6 +54,20 @@ } ] }, + { + "name": "addnodeowner", + "base": "", + "fields": [ + { + "name": "account_name", + "type": "name" + }, + { + "name": "tier", + "type": "uint8" + } + ] + }, { "name": "authority", "base": "", @@ -114,24 +128,6 @@ } ] }, - { - "name": "block_info_record", - "base": "", - "fields": [ - { - "name": "version", - "type": "uint8" - }, - { - "name": "block_height", - "type": "uint32" - }, - { - "name": "block_timestamp", - "type": "time_point" - } - ] - }, { "name": "block_signing_authority_v0", "base": "", @@ -220,6 +216,16 @@ } ] }, + { + "name": "claimnodedis", + "base": "", + "fields": [ + { + "name": "account_name", + "type": "name" + } + ] + }, { "name": "deleteauth", "base": "", @@ -252,6 +258,16 @@ } ] }, + { + "name": "emission_state", + "base": "", + "fields": [ + { + "name": "node_rewards_start", + "type": "time_point_sec" + } + ] + }, { "name": "fin_key_id_generator_info", "base": "", @@ -442,6 +458,50 @@ } ] }, + { + "name": "node_claim_result", + "base": "", + "fields": [ + { + "name": "total_allocation", + "type": "asset" + }, + { + "name": "claimed", + "type": "asset" + }, + { + "name": "claimable", + "type": "asset" + }, + { + "name": "can_claim", + "type": "bool" + } + ] + }, + { + "name": "node_owner_distribution", + "base": "", + "fields": [ + { + "name": "account_name", + "type": "name" + }, + { + "name": "total_allocation", + "type": "asset" + }, + { + "name": "claimed", + "type": "asset" + }, + { + "name": "total_duration", + "type": "uint32" + } + ] + }, { "name": "onblock", "base": "", @@ -740,6 +800,16 @@ } ] }, + { + "name": "setinittime", + "base": "", + "fields": [ + { + "name": "no_reward_init_time", + "type": "time_point_sec" + } + ] + }, { "name": "setparams", "base": "", @@ -895,6 +965,16 @@ } ] }, + { + "name": "viewnodedist", + "base": "", + "fields": [ + { + "name": "account_name", + "type": "name" + } + ] + }, { "name": "wait_weight", "base": "", @@ -1011,6 +1091,24 @@ "type": "public_key?" } ] + }, + { + "name": "block_info_record", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "block_height", + "type": "uint32" + }, + { + "name": "block_timestamp", + "type": "time_point" + } + ] } ], "actions": [ @@ -1024,6 +1122,16 @@ "type": "activate", "ricardian_contract": "" }, + { + "name": "addnodeowner", + "type": "addnodeowner", + "ricardian_contract": "" + }, + { + "name": "claimnodedis", + "type": "claimnodedis", + "ricardian_contract": "" + }, { "name": "deleteauth", "type": "deleteauth", @@ -1109,6 +1217,11 @@ "type": "setcode", "ricardian_contract": "" }, + { + "name": "setinittime", + "type": "setinittime", + "ricardian_contract": "" + }, { "name": "setparams", "type": "setparams", @@ -1154,6 +1267,11 @@ "type": "updateauth", "ricardian_contract": "" }, + { + "name": "viewnodedist", + "type": "viewnodedist", + "ricardian_contract": "" + }, { "name": "wasmcfg", "type": "wasmcfg", @@ -1184,8 +1302,8 @@ "key_types": [] }, { - "name": "blockinfo", - "type": "block_info_record", + "name": "emissionmngr", + "type": "emission_state", "index_type": "i64", "key_names": [], "key_types": [] @@ -1225,6 +1343,13 @@ "key_names": [], "key_types": [] }, + { + "name": "nodedist", + "type": "node_owner_distribution", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, { "name": "producers", "type": "producer_info", @@ -1245,6 +1370,13 @@ "index_type": "i64", "key_names": [], "key_types": [] + }, + { + "name": "blockinfo", + "type": "block_info_record", + "index_type": "i64", + "key_names": [], + "key_types": [] } ], "ricardian_clauses": [], @@ -1259,6 +1391,10 @@ } ], "action_results": [ + { + "name": "viewnodedist", + "result_type": "node_claim_result" + }, { "name": "getpeerkeys", "result_type": "getpeerkeys_res_t" diff --git a/contracts/sysio.system/sysio.system.wasm b/contracts/sysio.system/sysio.system.wasm index 78fccf52d55dc377bc224eb2e9a5d7955df87a5e..3c9d27a8d18aaaaf621c8c9e343b511acf85d228 100755 GIT binary patch delta 35875 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 E11m2_;{X5v From 4faa968e8b89b88503a8b28e4d73944cf9d7d104 Mon Sep 17 00:00:00 2001 From: dtaghavi Date: Wed, 17 Dec 2025 19:31:59 +0000 Subject: [PATCH 4/4] PR cleanup - Removed unused temp uint256 implementation - Adjusted addnodeowner to not require distribution start time to be set, leaving this restriction on claim. - Removed setinittime from roa test as adding a node owner doesn't need this restriction --- .../include/sysio.system/uint256.hpp | 121 ------------------ contracts/sysio.system/src/emissions.cpp | 4 - contracts/sysio.system/sysio.system.wasm | Bin 149430 -> 149195 bytes contracts/tests/emissions_tests.cpp | 14 -- contracts/tests/sysio.roa_tests.cpp | 9 -- 5 files changed, 148 deletions(-) delete mode 100644 contracts/sysio.system/include/sysio.system/uint256.hpp diff --git a/contracts/sysio.system/include/sysio.system/uint256.hpp b/contracts/sysio.system/include/sysio.system/uint256.hpp deleted file mode 100644 index 8dfc219b4e..0000000000 --- a/contracts/sysio.system/include/sysio.system/uint256.hpp +++ /dev/null @@ -1,121 +0,0 @@ -#include -#include -#include - -namespace wns { - - struct uint256_t { - uint128_t low; - uint128_t high; - - // Default constructor - uint256_t() : high(0), low(0) {} - - // Construct from uint128_t - uint256_t(uint128_t val) : high(0), low(val) {} - uint256_t(uint128_t low_val, uint128_t high_val): high(high_val), low(low_val) {} - - - // Addition with overflow check - uint256_t operator+(const uint256_t& other) const { - uint256_t result; - result.low = low + other.low; - result.high = high + other.high; - if (result.low < low) { // Check for overflow in low part - result.high++; // Carry to high part - } - return result; - } - - // Subtraction with underflow check - uint256_t operator-(const uint256_t& other) const { - uint256_t result; - result.low = low - other.low; - result.high = high - other.high; - if (result.low > low) { // Check for underflow in low part - result.high--; // Borrow from high part - } - return result; - } - - uint256_t& operator+=(const uint256_t& other) { - uint128_t new_low = low + other.low; - high += other.high; - if( new_low < low) { //Check for overflow - high++; //Carry the one - } - low = new_low; - return *this; - } - - uint256_t& operator -=(const uint256_t& other) { - uint128_t new_low = low - other.low; - high -= other.high; - if( new_low > low ) { // Check for underflow - high--; //Borrow one - } - low = new_low; - return *this; - } - - // Less than - bool operator<(const uint256_t& other) const { - if (high == other.high) { - return low < other.low; - } - return high < other.high; - } - - // Greater than - bool operator>(const uint256_t& other) const { - if (high == other.high) { - return low > other.low; - } - return high > other.high; - } - - // Equal to - bool operator==(const uint256_t& other) const { - return (high == other.high) && (low == other.low); - } - - // Less than or equal to - bool operator<=(const uint256_t& other) const { - return *this < other || *this == other; - } - - bool operator>=(const uint256_t& other) const { - return *this > other || *this == other; - } - - // Convert to hexadecimal string for easier printing and debugging - std::string to_string() const { - char buffer[65]; // 64 hex chars + null terminator - uint64_t high_high = (uint64_t)(high >> 64); - uint64_t high_low = (uint64_t)(high); - uint64_t low_high = (uint64_t)(low >> 64); - uint64_t low_low = (uint64_t)(low); - - snprintf(buffer, sizeof(buffer), - "%016llx%016llx%016llx%016llx", - high_high, high_low, low_high, low_low); - return std::string(buffer); - } - // Print method for sysio::print compatibility - void print() const { - sysio::print(to_string()); - } - - static uint256_t from_string(const std::string& str) { - uint128_t high = (uint128_t)std::stoull(str.substr(0, 16), nullptr, 16) << 64 | - (uint128_t)std::stoull(str.substr(16, 16), nullptr, 16); - uint128_t low = (uint128_t)std::stoull(str.substr(32, 16), nullptr, 16) << 64 | - (uint128_t)std::stoull(str.substr(48, 16), nullptr, 16); - - return uint256_t(low, high); - } - - // Serialization support - SYSLIB_SERIALIZE(uint256_t, (low)(high)) - }; -} \ No newline at end of file diff --git a/contracts/sysio.system/src/emissions.cpp b/contracts/sysio.system/src/emissions.cpp index f08edc75eb..0b3fd4865e 100644 --- a/contracts/sysio.system/src/emissions.cpp +++ b/contracts/sysio.system/src/emissions.cpp @@ -109,10 +109,6 @@ namespace sysiosystem { // Tier sanity check sysio::check(tier >= 1 && tier <=3, "invalid tier"); - // Get Emission state info - emissionstate_t emission_s(get_self(), get_self().value); - sysio::check(emission_s.exists(), "emission state not initialized"); - // Ensure this account isn't already in the table. nodedist_t nodedist(get_self(), get_self().value); auto itr = nodedist.find(account_name.value); diff --git a/contracts/sysio.system/sysio.system.wasm b/contracts/sysio.system/sysio.system.wasm index 3c9d27a8d18aaaaf621c8c9e343b511acf85d228..9058446e97f8009d98fda9ce36ffebf7db006604 100755 GIT binary patch delta 716 zcmYLGL1+_E5S>3i+jhw|d=X+*BtJ%oI-^&*H^ zDe}dG=tX+7CEyl4=-Kw5K~FX)6h*6tsKrB2aQ<#dFFWtgyqP!i_SqeK^|rmVVB@!^ zt$X&zbJ$yNU7kArfIVzI|MhQ{y^dC9*!W#+cl5p@8`I?;%hF?E`N^1`|HH)KnPE_% z+RQ@QxQJ>d+cc9O^pYU*px76n?zAX27iwQ4Y?8}FEF+Dwi&RJ3>2`BJz zC`N)%D!Fn7>teU$E|=%y%Rcv#gNXkXh&B~BNJ1A3@^M1H7{GF9GRBcN5J(W;sU+#> zrU~0NP8eK5uYf*|V;$>~Oso}{3H8P>NG7QwD3IB-mocqLujJoIMwa+kbBw#H8qntx zNhN|@sWk}1593@3n|b4`=_U<0L6bq8PA7WR0#af}9VDJ20_H*gTxB`EHi*0SSMxxF z%r>>Sm8Nwe7=`o(*UNM4O4J_0hJ}ASQ89yl+JNc3_&%%Odbme7JiM+w4|9C-;-r3j M7>`7`L#VU=0Y?v-=>Px# delta 854 zcmY*XO=uHA7@hfM(=;S03oU6>#A*7&1_FtfO^cUZjisXa10H%%FvLP!f{JZLMNL|J zD2Ny-GQm^4lpY%brg+h#;6agksY!bXtx!D_6%V0;-^^lcFFS8$-uJ!tzS;Lre6ODR zmKS`mTB$zwZJdSKM)mgii5GOS`ufM8Y5J+QlBH*!!CFHc?59H_)}kTYEL0YUMDaJ( zy{Ci3CiYrwrbU1Gx;DIw=IKM4?Wh9~{ z**Icc&VvOO*SIDgbi;wm9-E8>vB>^N9PZIKnE{%+8x2yFZ2TlT4NyPAW*wm)FW>Ex zvRtX*JyHo*$`Ex821X3ocpDqXu-0%!RTR}%cAZ;yyGqyGH6KPEM(GhF9vIAX%oSzD z$27^L&%(-f7v4!ql+DZ2aDYiyF0b6??W`Vd^GYrc{_~dGyrnm>HgC!0skmETlrhop zK;U90CSrDvV}hTsfeLm7_Qi<0>7+ot9e6_v`=86f^96O|cyulCgo=RDA?*M@^&`X#s-hR-3f9)~01 jQXFPO=}hLv^z|v8&fS_wU%ksG?p&Lknyl@{;X~*zTphQ3 diff --git a/contracts/tests/emissions_tests.cpp b/contracts/tests/emissions_tests.cpp index ba6ae9dcfe..a724d3c26d 100644 --- a/contracts/tests/emissions_tests.cpp +++ b/contracts/tests/emissions_tests.cpp @@ -508,19 +508,9 @@ BOOST_FIXTURE_TEST_CASE( setinittime_singleton_write_and_reprotect, sysio_emissi // addnodeowner // ----------------------------------------------------------------------------- -BOOST_FIXTURE_TEST_CASE( addnodeowner_requires_emission_initialized, sysio_emissions_tester ) try { - create_user_accounts({ "nodeowner1"_n }); - - auto r = addnodeowner( ROA, "nodeowner1"_n, 1 ); - BOOST_REQUIRE( r != success() ); - require_substr( r, "emission state not initialized" ); -} FC_LOG_AND_RETHROW() - BOOST_FIXTURE_TEST_CASE( addnodeowner_requires_sysio_roa_auth, sysio_emissions_tester ) try { create_user_accounts({ "alice"_n, "nodeowner2"_n }); - BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(head_secs()) ) ); - auto r = addnodeowner( "alice"_n, "nodeowner2"_n, 1 ); BOOST_REQUIRE( r != success() ); require_substr( r, "missing authority of sysio.roa" ); @@ -530,8 +520,6 @@ BOOST_FIXTURE_TEST_CASE( addnodeowner_rejects_invalid_tier, sysio_emissions_test // Tier must be 1..3 create_user_accounts({ "nodeowner3"_n }); - BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(head_secs()) ) ); - auto r1 = addnodeowner( ROA, "nodeowner3"_n, 0 ); BOOST_REQUIRE( r1 != success() ); require_substr( r1, "invalid tier" ); @@ -546,8 +534,6 @@ BOOST_FIXTURE_TEST_CASE( addnodeowner_writes_expected_rows_for_each_tier, sysio_ // Also verifies uniqueness constraint (account already exists). create_user_accounts({ "t1"_n, "t2"_n, "t3"_n }); - BOOST_REQUIRE_EQUAL( success(), setinittime( config::system_account_name, tpsec(head_secs()) ) ); - BOOST_REQUIRE_EQUAL( success(), addnodeowner( ROA, "t1"_n, 1 ) ); auto r1 = get_nodedist_row("t1"_n); BOOST_REQUIRE( !r1.is_null() ); diff --git a/contracts/tests/sysio.roa_tests.cpp b/contracts/tests/sysio.roa_tests.cpp index 615a3139f6..6bef388f54 100644 --- a/contracts/tests/sysio.roa_tests.cpp +++ b/contracts/tests/sysio.roa_tests.cpp @@ -408,15 +408,6 @@ BOOST_FIXTURE_TEST_CASE( verify_ram, sysio_roa_tester ) try { ("core", symbol(CORE_SYMBOL).to_string())); produce_block(); - // initialize emissions state so ROA inline addnodeowner can succeed - base_tester::push_action( - config::system_account_name, - "setinittime"_n, - config::system_account_name, - mvo()("no_reward_init_time", time_point_sec(control->head().block_time())) - ); - produce_blocks(1); - // roa has been activated with NODE_DADDY as a node owner // Accounts already created with ROA policy { "alice"_n, "bob"_n, "carol"_n, "darcy"_n } int64_t ram; int64_t net; int64_t cpu;