From 937ba74f6be22c0def7d88f26e47fcd7c50dd68f Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Thu, 24 Sep 2026 12:33:59 -0500 Subject: [PATCH] roa: carve sysio's account-creation RAM out of tier 1 at activation activateroa now funds sysio's pool with 1/10 of every tier-1 slot's allocation up front, instead of regnodeowner granting 1/10 of every tier's allocation one registration at a time through a permanently locked policy row. Tier 1 is the tier that creates accounts; tiers 2 and 3 no longer contribute and keep their whole allocation. The removed row did nothing: it could never be reduced or extended, and the budget debit and quota credit it described were applied directly. nodeowners.total_sys now records the owner's budget net of sysio's share, so an owner's allocated_sys is exactly the sum of the policies it has issued. Dropping the registration grant also removes its absolute re-sync of sysio's quota from the reslimit row, which refunded the gift taken by each plain newaccount since the previous registration. --- contracts/sysio.roa/sysio.roa.cpp | 87 +++++++++---------------- contracts/sysio.roa/sysio.roa.hpp | 15 +++-- contracts/sysio.roa/sysio.roa.wasm | Bin 53407 -> 52811 bytes contracts/tests/sysio.roa_tests.cpp | 96 ++++++++++++++++++---------- docs/roa-overview.md | 57 ++++++++--------- 5 files changed, 129 insertions(+), 126 deletions(-) diff --git a/contracts/sysio.roa/sysio.roa.cpp b/contracts/sysio.roa/sysio.roa.cpp index b4fb01dcec..7fea385e91 100644 --- a/contracts/sysio.roa/sysio.roa.cpp +++ b/contracts/sysio.roa/sysio.roa.cpp @@ -21,6 +21,9 @@ namespace sysio { /// creators, and sysio.roa is privileged, so node-owner claims must refuse them here. constexpr std::string_view RESERVED_SYSTEM_NAME_PREFIX = "sysio."; + /// sysio's account-creation RAM pool takes 1/10 of every tier-1 allocation, carved out at activateroa. + constexpr int64_t SYSIO_POOL_SHARE_DIVISOR{10}; + /// Maximum number of generated account names checked before newuser gives up. constexpr uint32_t MAX_ACCOUNT_NAME_ATTEMPTS{100}; @@ -77,6 +80,14 @@ namespace sysio { } } + // The slice of one owner's tier allocation carved out for sysio's pool. Only tier 1 contributes: + // it is the tier that creates accounts (newuser). The owner's budget is the remainder, so + // activateroa's carve-out and get_allocation_for_tier always partition the same total. + static int64_t tier_sysio_share(uint8_t tier, int64_t total_amount) { + if (tier != 1) return 0; + return tier_sys_allocation(tier, total_amount) / SYSIO_POOL_SHARE_DIVISOR; + } + // Every policy weight must be denominated in the core SYS symbol. asset arithmetic only checks // that operands share a symbol, and the affordability gate compares raw amounts, so without this // a weight in a different symbol/precision would be silently accepted and mis-scale the reserve @@ -207,6 +218,12 @@ namespace sysio { // Allocated sum int64_t allocated = t1_total + t2_total + t3_total; + // sysio's share of every tier-1 slot, registered or not. It stays inside `allocated`; the owners' + // budgets (get_allocation_for_tier) are the tier allocation minus this share. + int64_t sysio_carve = tier_sysio_share(1, total_amount) * sysiosystem::emissions::T1_MAX_NODE_OWNERS + + tier_sysio_share(2, total_amount) * sysiosystem::emissions::T2_MAX_NODE_OWNERS + + tier_sysio_share(3, total_amount) * sysiosystem::emissions::T3_MAX_NODE_OWNERS; + // Leftover int64_t leftover = total_amount - allocated; @@ -217,19 +234,18 @@ namespace sysio { // (positivity and the upper bound are already checked above), so `leftover` is non-negative. check(allocated <= total_amount, "Total SYS too small: node-owner reserve exceeds supply"); - // Convert the leftover (SYS units) to bytes and partition it so the grand total of all - // reslimits stays exactly total_sys * bytes_per_unit — nothing is minted on top: - // T = node-owner reserve (allocated above) + roa allocation + sysio pool. - // sysio.roa keeps half the leftover for its own (growing) bookkeeping tables; sysio gets - // the rest as THE pool that funds account creation and every other system contract's RAM - // (deployed via setsyscode/setsysabi, which gift the exact bytes out of this pool). Other - // system contracts are deliberately NOT pre-allocated here — they self-fund exactly. The - // only deduction is the sysio.acct account-creation bucket seed, taken out of sysio's - // share so it stays conserved. + // Partition so the grand total of all reslimits stays exactly total_sys * bytes_per_unit — + // nothing is minted on top: + // T = node-owner budgets (allocated - sysio_carve) + roa allocation + sysio pool. + // sysio.roa keeps half the leftover for its own (growing) bookkeeping tables. sysio gets the + // other half plus sysio_carve as THE pool that funds account creation and every other system + // contract's RAM (setsyscode/setsysabi gift the exact bytes out of it). The sysio.acct + // account-creation bucket seed is taken out of sysio's share so it stays conserved. + // bytes_per_unit divides newaccount_ram, so these products stay far inside uint64/int64. uint64_t leftover_bytes = (uint64_t)leftover * bytes_per_unit; // leftover >= 0, guarded above uint64_t roa_ram_bytes = leftover_bytes / 2; const uint64_t acct_seed_bytes = sysiosystem::newaccount_ram; - uint64_t sysio_gross = leftover_bytes - roa_ram_bytes; + uint64_t sysio_gross = leftover_bytes - roa_ram_bytes + (uint64_t)sysio_carve * bytes_per_unit; check(sysio_gross > acct_seed_bytes, "Leftover RAM too small for the account-creation seed"); uint64_t sysio_ram_bytes = sysio_gross - acct_seed_bytes; @@ -874,7 +890,7 @@ namespace sysio { check(nodeowner_count(get_self(), state.network_gen, tier) < tier_cap, "node owner tier cap reached"); - // Get the total SYS allocation for this tier + // The owner's budget: the tier allocation net of sysio's carve-out asset total_sys_allocation = get_allocation_for_tier(tier); // Only a tier-1 owner is provisioned a personal allocation here. Tier 1 is the sole tier @@ -905,12 +921,6 @@ namespace sysio { allocated_sys += personal_ram_weight; allocated_ram += personal_ram_weight; // RAM allocation - // 10% of total SYS goes to sysio for RAM - int64_t sysio_alloc_amount = total_sys_allocation.amount / 10; - asset sysio_allocation(sysio_alloc_amount, total_sys_allocation.symbol); - allocated_sys += sysio_allocation; - allocated_ram += sysio_allocation; // Also RAM allocation since it's for sysio policy - // Minimal default net/cpu for a tier-1 owner: 0.0500 SYS each. Zero for tiers 2 and 3. // Adding a zero asset below is a no-op, so the nodeowners totals stay correct for every // tier without branching the accounting. @@ -921,12 +931,6 @@ namespace sysio { policies_t policies(get_self(), owner.value); auto pol_key = policy_key{owner.value}; - name sysio_account = "sysio"_n; - auto sysio_pol_key = policy_key{sysio_account.value}; - asset zero_asset(0, state.total_sys.symbol); - - // Guard the two policies independently: the sysio RAM grant is created for every tier, so - // it must not sit behind the presence of the tier-1-only personal policy. if (provision_personal && !policies.contains(pol_key)) { // Create personal policy policies.emplace(get_self(), pol_key, roa::policies{ @@ -940,20 +944,6 @@ namespace sysio { }); } - if (!policies.contains(sysio_pol_key)) { - // Create sysio policy for RAM. Every tier contributes 10% of its allocation to the - // network RAM pool that funds newaccount_ram, so this is not tier-gated. - policies.emplace(get_self(), sysio_pol_key, roa::policies{ - .owner = sysio_account, - .issuer = owner, - .net_weight = zero_asset, - .cpu_weight = zero_asset, - .ram_weight = sysio_allocation, - .bytes_per_unit = state.bytes_per_unit, - .time_block = UINT32_MAX, // do not allow to be extended - }); - } - // Owner reslimits. Stack the node-owner personal allocation onto whatever the account already // holds. increase_reslimit CREATES the row when absent -- folding in the one-time newaccount_ram // gift, so a fresh node owner gets exactly newaccount_ram + personal_ram_bytes (identical to the @@ -974,24 +964,6 @@ namespace sysio { (int64_t)personal_ram_bytes, /*require_to_exist=*/false); set_resource_limits(owner, (int64_t)owner_res.ram_bytes, owner_res.net.amount, owner_res.cpu.amount); - // Sysio reslimit - reslimit_t sysioreslimit(get_self()); - auto sysio_res_key = reslimit_key{sysio_account.value}; - auto sysio_res = sysioreslimit.get(sysio_res_key, "sysio reslimit does not exist."); - - uint64_t sysio_bytes = sysio_allocation.amount * state.bytes_per_unit; - sysioreslimit.modify(get_self(), sysio_res_key, [&](auto& row) { - // Saturating add, matching increase_reslimit -- this is the one reslimit-row accumulator that - // does not route through that helper, so harden it the same way (no-op for realistic values). - row.ram_bytes = opp::safe::add_sat_u64(row.ram_bytes, sysio_bytes); - }); - - // Re-read to get updated value for set_resource_limits - sysio_res = sysioreslimit.get(sysio_res_key); - - // Update the RAM allocation, sysio is a system account so -1, -1 for net and cpu to maintain unlimited. - set_resource_limits(sysio_account, sysio_res.ram_bytes, -1, -1); - // Finally, record the node owner entry with the new fields nodeowners.emplace(get_self(), node_key, roa::nodeowners{ .owner = owner, @@ -1030,8 +1002,9 @@ namespace sysio { // Ensure the contract is active check(state.is_active, "Contract not active yet."); - // Same fractions/rounding as activateroa's reserve sizing (shared helper). - int64_t allocation_amount = tier_sys_allocation(tier, state.total_sys.amount); + // Same fractions/rounding as activateroa's reserve sizing, net of sysio's carve-out (shared helpers). + int64_t allocation_amount = tier_sys_allocation(tier, state.total_sys.amount) + - tier_sysio_share(tier, state.total_sys.amount); return asset(allocation_amount, state.total_sys.symbol); }; diff --git a/contracts/sysio.roa/sysio.roa.hpp b/contracts/sysio.roa/sysio.roa.hpp index 27733e8837..54502e4e2d 100644 --- a/contracts/sysio.roa/sysio.roa.hpp +++ b/contracts/sysio.roa/sysio.roa.hpp @@ -19,6 +19,9 @@ namespace sysio { /** * @brief Initializes sysio.roa, should be called as last step in Bios Boot Sequence, activating the ROA resource management system. * + * Reserves every node-owner tier slot and carves 1/10 of each tier-1 slot's allocation out to + * `sysio` up front as the account-creation RAM pool; node owners later register against the rest. + * * @param total_sys The total starting SYS of the network. * @param bytes_per_unit The amount of bytes .0001 SYS is worth, set in roastate table. If SYS precision is different, same concept applies, the single smallest unit of the core token. */ @@ -273,7 +276,7 @@ namespace sysio { struct [[sysio::table("nodeowners")]] nodeowners { name owner; // Node Owners account name. uint8_t tier; // Represents what tier they hold: 1, 2, or 3 - asset total_sys; // Total SYS alloted based on tier. + asset total_sys; // SYS alloted based on tier, net of sysio's tier-1 carve-out. asset allocated_sys; // Total SYS allocated via policies they issued. asset allocated_bw; // Total SYS allocated to CPU / NET. asset allocated_ram; // Total SYS allocated to RAM. @@ -452,11 +455,10 @@ namespace sysio { /** * @brief Registers 'owner' as a Node Owner scoped by network_gen, granting the tier's SYS - * allotment and contributing 10% of it to the network RAM pool. + * allotment (net of sysio's tier-1 share, which activateroa already carved out). * * Every tier gets a `nodeowners` row (the budget and the membership that gates policy - * issuance), a reslimit row, and a policy granting 10% of the tier allocation to `sysio` - * for the account-creation RAM pool. + * issuance) and a reslimit row. * * Only tier 1 additionally gets a personal self-issued policy. It is the only tier that can * call `newuser`, whose `sponsors` / `sponsorcount` rows are the sole writes in this @@ -519,9 +521,10 @@ namespace sysio { /** - * @brief A simple getter for totall allotted SYS based on tier number: 1, 2, 3. Matches rounding and logic used in activation. + * @brief A node owner's SYS budget for tier 1, 2, or 3: the tier allocation, net of sysio's + * carve-out for tier 1. Matches the rounding activateroa uses to size the reserve. * - * @return An asset containing the amount of SYS this tier gets + * @return An asset containing the amount of SYS a node owner of this tier may issue */ asset get_allocation_for_tier(uint8_t tier); diff --git a/contracts/sysio.roa/sysio.roa.wasm b/contracts/sysio.roa/sysio.roa.wasm index d52b477cb2a4a6bf8837ed06f2867e1e042183a2..708bf3277a0e4780993b8c1c469ecf0c9240bdc9 100755 GIT binary patch delta 6435 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 L4F|2dwW9nNzwVXZ diff --git a/contracts/tests/sysio.roa_tests.cpp b/contracts/tests/sysio.roa_tests.cpp index 37eb19ad7d..632b06738b 100644 --- a/contracts/tests/sysio.roa_tests.cpp +++ b/contracts/tests/sysio.roa_tests.cpp @@ -646,8 +646,10 @@ BOOST_FIXTURE_TEST_CASE( verify_ram, sysio_roa_tester ) try { // verify initial conditions of ROA accounts control->get_resource_limits_manager().get_account_limits( "sysio"_n, ram, net, cpu ); - const int64_t roa_sysio_ram = initial_sysio_ram + 6281267200; - BOOST_TEST(ram == roa_sysio_ram); // ram after all tier-1 nodeowners + // Registration no longer grants sysio anything (activateroa carved its share out up front), so the + // reslimit row is unchanged. The quota is lower by the gift each plain newaccount above drew from it. + const int64_t roa_sysio_ram = initial_sysio_ram; + BOOST_TEST(ram == roa_sysio_ram - (int64_t)(node_owners.size() - 1) * (int64_t)newaccount_ram); control->get_resource_limits_manager().get_account_limits( "sysio.roa"_n, ram, net, cpu ); BOOST_TEST(ram == 157021280); // ram of roa itself control->get_resource_limits_manager().get_account_limits( "sysio.acct"_n, ram, net, cpu ); @@ -838,12 +840,8 @@ BOOST_FIXTURE_TEST_CASE( extend_policy_test, sysio_roa_tester ) try { BOOST_TEST(p["issuer"].as_string() == "alice"); BOOST_TEST(p["bytes_per_unit"].as_string() == "104"); BOOST_TEST(p["time_block"].as_string() == "1"); - p = get_policy("sysio"_n, "alice"_n); - BOOST_TEST(p["owner"].as_string() == "sysio"); - BOOST_TEST(p["issuer"].as_string() == "alice"); - BOOST_TEST(p["net_weight"].as_string() == "0.0000 SYS"); - BOOST_TEST(p["time_block"].as_string() == "4294967295"); - BOOST_TEST(p["ram_weight"].as_string() == "301.9840 SYS"); + // Registration writes no sysio grant policy; activateroa funded sysio's pool up front. + BOOST_TEST(get_policy("sysio"_n, "alice"_n).is_null()); // extend policy extend_policy("alice"_n, "alice"_n, 42); @@ -852,24 +850,21 @@ BOOST_FIXTURE_TEST_CASE( extend_policy_test, sysio_roa_tester ) try { BOOST_TEST(p["owner"].as_string() == "alice"); BOOST_TEST(p["issuer"].as_string() == "alice"); BOOST_TEST(p["time_block"].as_string() == "42"); - p = get_policy("sysio"_n, "alice"_n); - BOOST_TEST(p["owner"].as_string() == "sysio"); - BOOST_TEST(p["issuer"].as_string() == "alice"); - BOOST_TEST(p["net_weight"].as_string() == "0.0000 SYS"); - BOOST_TEST(p["time_block"].as_string() == "4294967295"); - result = extend_policy("sysio"_n, "alice"_n, 42); + result = extend_policy("alice"_n, "alice"_n, 42); BOOST_REQUIRE_EQUAL(error("assertion failure with message: Cannot reduce a policies existing time_block"), result); - BOOST_CHECK_EXCEPTION(reduce_roa_policy("alice"_n, "sysio"_n, "0.0000 SYS", "0.0000 SYS", "500.0000 SYS", 0), - sysio_assert_message_exception, - sysio_assert_message_is("Cannot reduce policy before time_block")); + result = extend_policy("sysio"_n, "alice"_n, 42); + BOOST_REQUIRE_EQUAL(error("assertion failure with message: Policy does not exist under this issuer for this owner"), result); - expand_roa_policy("alice"_n, "sysio"_n, "0.0000 SYS", "0.0000 SYS", "500.0000 SYS", 0); + // A node owner may still grant sysio extra RAM voluntarily, as an ordinary RAM-only policy. + add_roa_policy("alice"_n, "sysio"_n, "0.0000 SYS", "0.0000 SYS", "500.0000 SYS", 0, 0); + expand_roa_policy("alice"_n, "sysio"_n, "0.0000 SYS", "0.0000 SYS", "1.0000 SYS", 0); p = get_policy("sysio"_n, "alice"_n); BOOST_TEST(p["cpu_weight"].as_string() == "0.0000 SYS"); - BOOST_TEST(p["ram_weight"].as_string() == "801.9840 SYS"); + BOOST_TEST(p["ram_weight"].as_string() == "501.0000 SYS"); + BOOST_TEST(p["time_block"].as_string() == "0"); } FC_LOG_AND_RETHROW() @@ -1791,7 +1786,7 @@ BOOST_FIXTURE_TEST_CASE( newuser_tier2_fails, sysio_roa_full_tester ) try { // Only tier 1 is provisioned a personal policy at registration -- it is the sole tier that can call // newuser, whose sponsorship rows are the only writes billed to a node owner. Tiers 2 and 3 get the -// nodeowners budget, a reslimit row, and the 10% sysio RAM grant, but no allocation of their own, +// nodeowners budget and a reslimit row, but no allocation of their own, // and can still self-issue afterwards because addpolicy costs the issuer nothing. BOOST_FIXTURE_TEST_CASE( regnodeowner_personal_policy_is_tier1_only, sysio_roa_full_tester ) try { // The fixture already fills all 21 tier-1 slots, so reuse one rather than registering another. @@ -1812,20 +1807,21 @@ BOOST_FIXTURE_TEST_CASE( regnodeowner_personal_policy_is_tier1_only, sysio_roa_f BOOST_TEST(get_policy("t2owner"_n, "t2owner"_n).is_null()); BOOST_TEST(get_policy("t3owner"_n, "t3owner"_n).is_null()); - // Every tier still contributes 10% of its allocation to the sysio RAM pool. - for (auto owner : {t1owner, "t2owner"_n, "t3owner"_n}) { - auto grant = get_policy("sysio"_n, owner); - BOOST_REQUIRE(!grant.is_null()); + // No tier writes a sysio grant policy. Tier 1's budget is net of sysio's 1/10 share; tiers 2 and 3 + // contribute nothing and keep their whole allocation. Launch fixture: 75,496 SYS supply. + const std::array, 3> budgets{{ + {t1owner, 30'198'400 - 3'019'840}, // T1: 4% of supply, less sysio's share + {"t2owner"_n, 1'132'440}, // T2: 0.15% + {"t3owner"_n, 22'649}, // T3: 0.003% + }}; + for (const auto& [owner, budget] : budgets) { + BOOST_TEST(get_policy("sysio"_n, owner).is_null()); auto node = get_nodeowner(owner); BOOST_REQUIRE(!node.is_null()); - BOOST_TEST(grant["ram_weight"].as().get_amount() - == node["total_sys"].as().get_amount() / 10); - BOOST_TEST(grant["net_weight"].as().get_amount() == 0); - BOOST_TEST(grant["cpu_weight"].as().get_amount() == 0); + BOOST_TEST(node["total_sys"].as().get_amount() == budget); } - // Tier 2/3 hold no bandwidth, and their nodeowners accounting excludes the personal weights, - // so the full remainder of the tier budget stays issuable. + // Tier 2/3 hold no bandwidth and have allocated nothing, so their whole budget stays issuable. for (auto owner : {"t2owner"_n, "t3owner"_n}) { int64_t ram, net, cpu; control->get_resource_limits_manager().get_account_limits(owner, ram, net, cpu); @@ -1833,10 +1829,9 @@ BOOST_FIXTURE_TEST_CASE( regnodeowner_personal_policy_is_tier1_only, sysio_roa_f BOOST_TEST(cpu == 0); auto node = get_nodeowner(owner); - const int64_t total = node["total_sys"].as().get_amount(); BOOST_TEST(node["allocated_bw"].as().get_amount() == 0); - BOOST_TEST(node["allocated_ram"].as().get_amount() == total / 10); - BOOST_TEST(node["allocated_sys"].as().get_amount() == total / 10); + BOOST_TEST(node["allocated_ram"].as().get_amount() == 0); + BOOST_TEST(node["allocated_sys"].as().get_amount() == 0); } // A tier-3 owner can still issue to itself; sysio.roa pays both the CPU/NET and the row RAM. @@ -2660,6 +2655,41 @@ BOOST_FIXTURE_TEST_CASE( activateroa_accepts_normal_supply, roa_unactivated_test BOOST_REQUIRE_NO_THROW( activate("75496.0000 SYS", 104) ); } FC_LOG_AND_RETHROW() +// activateroa carves sysio's 1/10 share of every tier-1 slot into its pool up front, and the pools plus +// every node-owner budget partition the supply exactly -- no bytes minted or lost. +BOOST_FIXTURE_TEST_CASE( activateroa_carves_sysio_pool, roa_unactivated_tester ) try { + constexpr int64_t total_units = 754'960'000; // 75,496.0000 SYS + constexpr int64_t bytes_per_unit = 104; + activate("75496.0000 SYS", bytes_per_unit); + + auto quota = [&](account_name a) { + int64_t ram, net, cpu; + control->get_resource_limits_manager().get_account_limits(a, ram, net, cpu); + return ram; + }; + const int64_t roa_ram = quota(ROA); + const int64_t sysio_ram = quota(config::system_account_name); + const int64_t acct_ram = quota("sysio.acct"_n); + + // Per-slot allocation and sysio's share, by tier: {count, allocation, share}. Only tier 1 contributes. + constexpr std::array, 3> tiers{{ + {21, 30'198'400, 3'019'840}, + {84, 1'132'440, 0}, + {1000, 22'649, 0}, + }}; + int64_t carve = 0, budgets = 0; + for (const auto& [count, alloc, share] : tiers) { + carve += count * share; + budgets += count * (alloc - share); + } + + BOOST_TEST(roa_ram == 157'021'280); + BOOST_TEST(acct_ram == (int64_t)newaccount_ram); + BOOST_TEST(sysio_ram == roa_ram - acct_ram + carve * bytes_per_unit); // 6.75 GB + BOOST_TEST(sysio_ram == 6'752'350'696); + BOOST_TEST(roa_ram + sysio_ram + acct_ram + budgets * bytes_per_unit == total_units * bytes_per_unit); +} FC_LOG_AND_RETHROW() + // A supply above the bound is rejected before the tier math (total_amount * 15, leftover * // bytes_per_unit) could overflow int64. Defense in depth -- activateroa is a one-time governance call // and real supplies are ~1e6x smaller. diff --git a/docs/roa-overview.md b/docs/roa-overview.md index a0d049abec..5dd9363aef 100644 --- a/docs/roa-overview.md +++ b/docs/roa-overview.md @@ -317,42 +317,40 @@ node owner has exactly the same policy powers as a tier-1 node owner. What diffe | 2 | 0.15% | 84 | 12.6% | | 3 | 0.003% | 1,000 | 3.0% | -Registration consumes part of an owner's own budget — 10% of the tier allocation set aside into -the network RAM pool, plus a flat personal policy for the owner's own account: 0.0080 SYS of RAM -and 0.0500 SYS each of NET and CPU, 0.1080 SYS in total — **tier 1 only**. Tiers 2 and 3 receive -no personal policy: managing policies costs an owner nothing, so they need no allocation of their -own and keep the whole remainder issuable. Using the launch configuration of 75,496 SYS -`total_sys`: - -| Tier | Total allocation | Free to issue after registration | ≈ RAM if spent entirely on RAM | +`activateroa` carves 10% of every tier-1 slot's allocation out to `sysio`'s RAM pool up front — tier 1 +is the tier that creates accounts — so a tier-1 owner's budget (`nodeowners.total_sys`) is the other +90%. Tiers 2 and 3 contribute nothing and keep their whole allocation. Registration then spends a flat +personal policy for the owner's own account: 0.0080 SYS of RAM and 0.0500 SYS each of NET and CPU, +0.1080 SYS in total — **tier 1 only**. Tiers 2 and 3 receive no personal policy: managing policies +costs an owner nothing, so they need no allocation of their own and keep the whole budget issuable. +Using the launch configuration of 75,496 SYS `total_sys`: + +| Tier | Tier allocation | Owner budget (`total_sys`) | ≈ RAM if spent entirely on RAM | |---|---|---|---| -| 1 | 3,019.8400 SYS | ~2,718 SYS | ~2.8 GB | -| 2 | 113.2440 SYS | ~102 SYS | ~106 MB | -| 3 | 2.2649 SYS | ~2.04 SYS | ~2.1 MB | +| 1 | 3,019.8400 SYS | 2,717.8560 SYS | ~2.8 GB | +| 2 | 113.2440 SYS | 113.2440 SYS | ~118 MB | +| 3 | 2.2649 SYS | 2.2649 SYS | ~2.4 MB | Every `addpolicy` and `expandpolicy` checks `total_new_allocation <= node.total_sys - node.allocated_sys`. A node owner cannot issue more than they hold. ### What registration provisions -`regnodeowner` spends part of the tier allocation before the owner has issued anything: +`regnodeowner` spends part of a tier-1 owner's budget before the owner has issued anything: | Component | Amount | Scales with tier | |---|---|---| -| `sysio` RAM pool grant | 10% of the tier allocation | Yes — every tier | | Personal RAM | 0.0080 SYS (8,320 bytes) | Tier 1 only | | Personal NET | 0.0500 SYS | Tier 1 only | | Personal CPU | 0.0500 SYS | Tier 1 only | -The 10% grant is not for the owner. It moves bytes into `sysio`'s RAM pool, which funds the -1,144-byte gift every new account on the network receives. It is written with -`time_block = UINT32_MAX` and is never reclaimable. +The three land in a self-issued policy — `issuer == owner` — carrying `time_block = 1`, so the +owner can reshape or reclaim them immediately with `expandpolicy` or `reducepolicy`. Tier 1 gets +them because it is the only tier that can call `newuser`, whose `sponsors` and `sponsorcount` rows +are the only writes in the contract billed to a node owner. -A tier-1 owner's three personal components land in a self-issued policy — `issuer == owner` — -carrying `time_block = 1`, so it can reshape or reclaim them immediately with `expandpolicy` or -`reducepolicy`. Tier 1 gets them because it is the only tier that can call `newuser`, whose -`sponsors` and `sponsorcount` rows are the only writes in the contract billed to a node owner. The -10% grant is not tier-gated. +Registration writes no policy for `sysio`. Its tier-1 share was carved out at activation, so an +owner's `allocated_sys` is exactly the sum of the policies it has issued. ### What a node owner needs to operate @@ -375,7 +373,7 @@ owner needs RAM headroom before its first `newuser` call. | | Tier 1 | Tier 2 | Tier 3 | |---|---|---|---| | Issue, expand, extend, reduce policies | Yes | Yes | Yes | -| Budget per owner | 3,019.8400 SYS | 113.2440 SYS | 2.2649 SYS | +| Budget per owner | 2,717.8560 SYS | 113.2440 SYS | 2.2649 SYS | | Max owners | 21 | 84 | 1,000 | | `newuser` (sponsored accounts) | Yes | No | No | @@ -385,16 +383,15 @@ actions. `newuser` is the only tier-gated capability, guarded by ### Where network RAM comes from -`activateroa` splits the SYS left over after all tier allocations between two pools: +`activateroa` funds two pools: -| Pool | Size | Funds | +| Pool | Size at activation | Funds | |---|---|---| -| `sysio.roa` | ~157 MB at activation | The contract's own rows: policies, reslimits, node-owner records | -| `sysio` | ~157 MB at activation, ~7.98 GB once every node owner has registered | The 1,144-byte gift every new account receives | +| `sysio.roa` | ~157 MB — half the SYS left over after all tier allocations | The contract's own rows: policies, reslimits, node-owner records | +| `sysio` | ~6.75 GB — the other half, plus 10% of every tier-1 slot's allocation | The 1,144-byte gift every new account receives | -`sysio`'s pool grows as owners register, because each registration deposits 10% of its tier -allocation into it. At 1,144 bytes per account, the funded pool supports roughly 6.97 million -accounts. +`sysio`'s pool is fully funded at activation, whether or not any node owner has registered. At +1,144 bytes per account it supports roughly 5.90 million accounts. The gap between those two pools is why `newuser` bills its sponsorship rows to the sponsoring tier-1 owner rather than to the contract. Billed to the contract, `sysio.roa`'s ~157 MB would cap @@ -406,7 +403,7 @@ A sponsorship costs **144 bytes** — a 16-byte key and 16-byte value over `billable_size_v` of 112 — plus a one-time 144-byte `sponsorcount` row on a creator's first `newuser`. Neither pool is a ceiling: `addpolicy` refuses CPU and NET to a `sysio.`-prefixed account but not RAM, so a node owner can extend either with a RAM-only policy. The ~157 MB is -where `activateroa` starts them. +where `activateroa` starts `sysio.roa`. ### The four policy actions