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 0fb55be1f4..8fc989d5da 100755 Binary files a/contracts/sysio.roa/sysio.roa.wasm and b/contracts/sysio.roa/sysio.roa.wasm differ 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