diff --git a/contracts/sysio.roa/sysio.roa.hpp b/contracts/sysio.roa/sysio.roa.hpp index d2b9820b13..c98b48f17c 100644 --- a/contracts/sysio.roa/sysio.roa.hpp +++ b/contracts/sysio.roa/sysio.roa.hpp @@ -51,8 +51,12 @@ namespace sysio { * @param cpu_weight The amount of SYS allocated for CPU * @param ram_weight The amount of SYS allocated for RAM. * @param time_block A block number, the policy can't be reclaimed or reduced before this block. + * A value at or below the current block issues the grant unlocked (reducible + * immediately); extendpolicy adds or lengthens a lock. * @param network_gen Generation of issuer, in cases where you are a Node Owner in multiple, - * specifies which allocation of SYS to pull from. + * specifies which allocation of SYS to pull from. Must not exceed the + * current generation, which is always 0: only the one-shot activateroa + * sets it. */ [[sysio::action]] void addpolicy(const name& owner, const name& issuer, const asset& net_weight, const asset& cpu_weight, const asset& ram_weight, const uint32_t& time_block, const uint8_t& network_gen); @@ -68,7 +72,9 @@ namespace sysio { * @param cpu_weight The amount in SYS to increase CPU by. * @param ram_weight The amount in SYS to increase RAM by. * @param network_gen Generation of issuer, in cases where you are a Node Owner in multiple, - * specifies which allocation of SYS to pull from. + * specifies which allocation of SYS to pull from. Must not exceed the + * current generation, which is always 0: only the one-shot activateroa + * sets it. */ [[sysio::action]] void expandpolicy(const name& owner, const name& issuer, const asset& net_weight, const asset& cpu_weight, const asset& ram_weight, const uint8_t& network_gen); @@ -85,7 +91,7 @@ namespace sysio { /** * @brief Decrease the resource limits on an existing policy. Subtracts new weights from - * existing values. Only callable after policy's time_block. + * existing values. Only callable once the current block reaches the policy's time_block. * * Note: Will reclaim UPTO ram_weight worth of bytes, limited to the pool of unused bytes on 'owner's reslimit and upper bound by the policy ram_weight. * @@ -100,7 +106,9 @@ namespace sysio { * @param cpu_weight The non-negative amount in SYS to decrease CPU by. * @param ram_weight The non-negative amount in SYS to attempt decreasing RAM by, returning only * @param network_gen Generation of issuer, in cases where you are a Node Owner in multiple, - * specifies which allocation of SYS to adjust. + * specifies which allocation of SYS to adjust. Must not exceed the + * current generation, which is always 0: only the one-shot activateroa + * sets it. */ [[sysio::action]] void reducepolicy(const name& owner, const name& issuer, const asset& net_weight, const asset& cpu_weight, const asset& ram_weight, const uint8_t& network_gen); @@ -327,6 +335,11 @@ namespace sysio { SYSLIB_SERIALIZE(policy_key, (owner)) }; + /** + * A row does not record the network generation that funded it; expandpolicy and reducepolicy + * adjust whichever generation's budget the caller names. A future generation rotation must add + * that field before those two actions can be generation-safe. + */ struct [[sysio::table("policies")]] policies { name owner; // Account name this policy applies to. name issuer; // Account name of the Node Owner who issued this policy. diff --git a/contracts/sysio.system/include/sysio.system/sysio.system.hpp b/contracts/sysio.system/include/sysio.system/sysio.system.hpp index 265ec0c2d2..07f630c368 100644 --- a/contracts/sysio.system/include/sysio.system/sysio.system.hpp +++ b/contracts/sysio.system/include/sysio.system/sysio.system.hpp @@ -170,9 +170,9 @@ namespace sysiosystem { /// the period's per-block rate and zeroes the count of every producer it pays; a block the /// producer's slot did not deliver is simply never counted, so its pay stays in the treasury. /// At most one round per rotation is counted; see `last_paid_round`. - /// The count SURVIVES a park, a demotion, or a lost key: a producer that is not schedulable - /// at a payepoch is neither paid nor reset, and whatever was counted is paid at the first - /// payepoch where it is schedulable again (never, for a slashed or terminated one). + /// The count SURVIVES a park, a demotion, a lost key, or a termination: a producer that is not + /// schedulable at a payepoch is neither paid nor reset, and whatever was counted is paid at the + /// first payepoch where it is schedulable again (never, for a slashed one). uint32_t unpaid_blocks = 0; time_point last_claim_time; /// Schedule-order key: `update_ranked_producers` sorts a new set of scheduled producers by diff --git a/contracts/tests/emissions_tests.cpp b/contracts/tests/emissions_tests.cpp index 4540aecb66..06a12f09f8 100644 --- a/contracts/tests/emissions_tests.cpp +++ b/contracts/tests/emissions_tests.cpp @@ -61,6 +61,44 @@ static constexpr account_name EPOCH = "sysio.epoch"_n; static constexpr account_name CHALG = "sysio.chalg"_n; static constexpr account_name MSGCH = "sysio.msgch"_n; +/// sysio.opreg actions the producer collateral and held-pay tests push. +namespace opreg_action { +constexpr auto regoperator = "regoperator"_n; +constexpr auto deposit = "deposit"_n; +constexpr auto withdraw = "withdraw"_n; +constexpr auto cancelwtdw = "cancelwtdw"_n; +constexpr auto claimremit = "claimremit"_n; +} // namespace opreg_action + +/// sysio.opreg tables the held-pay tests read, with the ABI type of a decoded row. +namespace opreg_table { +constexpr auto opcounters = "opcounters"_n; +constexpr char opcounters_row[] = "op_counters"; +constexpr auto wtdwqueue = "wtdwqueue"_n; +} // namespace opreg_table + +/// sysio.opreg action and row fields the producer collateral and held-pay tests use. +namespace opreg_field { +constexpr char account[] = "account"; +constexpr char type[] = "type"; +constexpr char is_bootstrapped[] = "is_bootstrapped"; +constexpr char token_code[] = "token_code"; +constexpr char amount[] = "amount"; +constexpr char request_id[] = "request_id"; +constexpr char status[] = "status"; +constexpr char next_withdraw_id[] = "next_withdraw_id"; +} // namespace opreg_field + +/// sysio.epoch `epochstate` field the held-pay payout check reads. +namespace epoch_field { +constexpr char current_epoch_index[] = "current_epoch_index"; +} // namespace epoch_field + +/// sysio.system `epochlog` field the held-pay payout check reads. +namespace epochlog_field { +constexpr char compute_amount[] = "compute_amount"; +} // namespace epochlog_field + // 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; @@ -231,6 +269,8 @@ static constexpr uint32_t T_STANDBY_END_RANK = 28; static constexpr uint16_t T_STANDBY_BPS = 800; /// The fixture's default epoch (init_epoch_state) is 60s at one block per 500ms. static constexpr uint32_t T_EPOCH_SECS = 60; +/// Seconds in one T5 emission epoch (24 hours). +static constexpr uint32_t ONE_EPOCH = 86400u; // Helper: amount NOT transferred at payepoch when no producers / batch // members are paid. Equals producer_pool + batch_pool (compute share, both @@ -2487,9 +2527,6 @@ BOOST_AUTO_TEST_SUITE_END() // sysio_emissions_tests BOOST_AUTO_TEST_SUITE(t5_emissions_tests) -// Helper: number of seconds in one epoch (24 hours) -static constexpr uint32_t ONE_EPOCH = 86400u; - // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- @@ -5627,8 +5664,14 @@ struct producer_score_tester : public producer_eligibility_tester { base_tester::push_action("sysio.liq"_n, "mint"_n, "sysio.synd"_n, mvo()("to", account)("token_code", token)("amount", amount)); } - return push_opreg_action(account, "deposit"_n, - mvo()("account", account)("token_code", token)("amount", amount)); + return deposit_collateral(account, amount, token); + } + + /// Bond `amount` of `token` the account already holds. + action_result deposit_collateral(account_name account, uint64_t amount, + std::string_view token = collateral_token) { + return push_opreg_action(account, opreg_action::deposit, mvo() + (opreg_field::account, account)(opreg_field::token_code, token)(opreg_field::amount, amount)); } /// Push `setscorecfg`. Defaults mirror the contract's own so a test names only the weight it @@ -5834,6 +5877,109 @@ struct producer_score_tester : public producer_eligibility_tester { } }; +/// Bonded producers on a published schedule, for tests that terminate one, bring it back and +/// check which payouts pay its held blocks. +struct terminated_producer_tester : public producer_score_tester { + + /// Producers on the published schedule. + static constexpr uint32_t producer_count = 3; + + /// Full rotations produced before the first payout, so every producer holds counted blocks. + static constexpr uint32_t warmup_rotations = 2; + + /// T5 starts this many seconds more than one T5 epoch ago, so the first advance pays. + static constexpr uint32_t t5_start_overdue_secs = 1; + + /// Blocks produced beyond one sysio.epoch of slots, so the next advance is past the boundary. + static constexpr uint32_t epoch_crossing_margin_blocks = 10; + + /// Bond the producers, publish their schedule, produce the warm-up rotations and start T5. + std::vector setup_paid_producers() { + create_t5_holding_accounts(); + auto names = setup_collateralized_producers(producer_count); + BOOST_REQUIRE_EQUAL( success(), set_producer_schedule(names) ); + produce_blocks(1); + wait_for_producer_schedule(); + produce_complete_cycles(producer_count, warmup_rotations); + BOOST_REQUIRE_EQUAL( success(), initt5( config::system_account_name, + tpsec(head_secs() - ONE_EPOCH - t5_start_overdue_secs) ) ); + return names; + } + + /// Produce past the next sysio.epoch boundary, so the next advance may run. + void cross_epoch_boundary() { + produce_blocks(test_nominal_slots(T_EPOCH_SECS) + epoch_crossing_margin_blocks); + } + + OperatorStatus operator_status(account_name account) { + return get_opreg_operator(account)[opreg_field::status].as(); + } + + /// Register `account` as a producer operator under its own authority, as an operator would. + action_result regoperator(account_name account) { + return push_opreg_action(account, opreg_action::regoperator, mvo() + (opreg_field::account, account) + (opreg_field::type, OperatorType::OPERATOR_TYPE_PRODUCER) + (opreg_field::is_bootstrapped, false)); + } + + action_result withdraw(account_name account, uint64_t amount) { + return push_opreg_action(account, opreg_action::withdraw, mvo() + (opreg_field::account, account)(opreg_field::token_code, collateral_token)(opreg_field::amount, amount)); + } + + action_result cancelwtdw(account_name account, uint64_t request_id) { + return push_opreg_action(account, opreg_action::cancelwtdw, mvo() + (opreg_field::account, account)(opreg_field::request_id, request_id)); + } + + action_result claimremit(account_name account) { + return push_opreg_action(account, opreg_action::claimremit, mvo() + (opreg_field::account, account)(opreg_field::token_code, collateral_token)); + } + + /// The id `withdraw` gave the latest request: one below opreg's next free id. + uint64_t last_withdraw_request_id() { + const auto data = get_row_by_account(OPREG, OPREG, opreg_table::opcounters, opreg_table::opcounters); + BOOST_REQUIRE( !data.empty() ); + const auto counters = opreg_abi_ser.binary_to_variant(opreg_table::opcounters_row, data, + fixture_abi_yield_function()); + return counters[opreg_field::next_withdraw_id].as() - 1; + } + + bool withdraw_request_queued(uint64_t request_id) { + return !get_row_by_id(OPREG, OPREG, opreg_table::wtdwqueue, request_id).empty(); + } + + /// A payout `producer` is not payable at pays it nothing and keeps its count. + void assert_unpaid_payout(account_name producer) { + const uint32_t blocks = unpaid_blocks_of(producer); + const asset before = get_wire_balance_paid(producer); + BOOST_REQUIRE_EQUAL( success(), advance_epoch_state() ); + BOOST_REQUIRE_EQUAL( get_wire_balance_paid(producer), before ); + BOOST_REQUIRE_GE( unpaid_blocks_of(producer), blocks ); + } + + /// The next payout pays `producer` every block it holds, at least `held`, at that period's rate. + void assert_payout_pays_held_blocks(const std::vector& names, account_name producer, + uint32_t held) { + uint64_t produced = 0; + for (const auto& p : names) produced += unpaid_blocks_of(p); + const uint32_t blocks = unpaid_blocks_of(producer); + BOOST_REQUIRE_GE( blocks, held ); + const asset before = get_wire_balance_paid(producer); + BOOST_REQUIRE_EQUAL( success(), advance_epoch_state() ); + const int64_t got = get_wire_balance_paid(producer).get_amount() - before.get_amount(); + // The payout logs the epoch the advance moved to. + const auto paid_epoch = get_epoch_state_row()[epoch_field::current_epoch_index].as(); + const int64_t active_pool = + test_active_pool(get_epoch_log(paid_epoch)[epochlog_field::compute_amount].as()); + const uint64_t divisor = std::max(produced, test_nominal_slots(T_EPOCH_SECS)); + BOOST_REQUIRE_EQUAL( got, test_block_pay(active_pool, blocks, divisor) ); + BOOST_REQUIRE_GT( got, 0 ); + } +}; + BOOST_AUTO_TEST_SUITE(sysio_producer_score_tests) // ACTIVE is role-specific at schedule time too. Force a full rescore after replacing a settled @@ -6450,6 +6596,60 @@ BOOST_FIXTURE_TEST_CASE( pruned_operator_row_stays_demoted_through_rescore_sweep BOOST_REQUIRE(!is_scheduled(target)); } FC_LOG_AND_RETHROW() +// Termination keeps a producer's counted blocks but credits its bond to its claim, and a new +// registration starts UNKNOWN with no bond, so the blocks are paid only once it bonds again. +BOOST_FIXTURE_TEST_CASE( terminated_producer_is_paid_held_blocks_once_bonded_again, + terminated_producer_tester ) try { + const auto names = setup_paid_producers(); + const auto target = names[0]; + BOOST_REQUIRE_EQUAL( success(), terminate_operator(target) ); + BOOST_REQUIRE( OperatorStatus::OPERATOR_STATUS_TERMINATED == operator_status(target) ); + const uint32_t held = unpaid_blocks_of(target); + BOOST_REQUIRE_GT( held, 0u ); + assert_unpaid_payout(target); + + // Pull the returned bond and register again: the new registration holds no bond. + const asset unclaimed = get_wire_balance(target); + BOOST_REQUIRE_EQUAL( success(), claimremit(target) ); + BOOST_REQUIRE_EQUAL( static_cast(base_min_bond), + get_wire_balance(target).get_amount() - unclaimed.get_amount() ); + BOOST_REQUIRE_EQUAL( success(), regoperator(target) ); + BOOST_REQUIRE( OperatorStatus::OPERATOR_STATUS_UNKNOWN == operator_status(target) ); + cross_epoch_boundary(); + assert_unpaid_payout(target); + + // Bond again: the next payout pays every held block at that period's rate. + BOOST_REQUIRE_EQUAL( success(), deposit_collateral(target, base_min_bond) ); + BOOST_REQUIRE( OperatorStatus::OPERATOR_STATUS_ACTIVE == operator_status(target) ); + cross_epoch_boundary(); + assert_payout_pays_held_blocks(names, target, held); +} FC_LOG_AND_RETHROW() + +// A withdrawal queued before termination outlives it, and the queue is per account: it reserves part +// of the new registration's bond until it is cancelled. +BOOST_FIXTURE_TEST_CASE( stale_withdrawal_holds_back_a_rebonded_producer_until_cancelled, + terminated_producer_tester ) try { + constexpr uint64_t stale_amount = base_min_bond / 2; // any part of the bond + const auto names = setup_paid_producers(); + const auto target = names[0]; + BOOST_REQUIRE_EQUAL( success(), withdraw(target, stale_amount) ); + const uint64_t stale_request = last_withdraw_request_id(); + BOOST_REQUIRE_EQUAL( success(), terminate_operator(target) ); + BOOST_REQUIRE( withdraw_request_queued(stale_request) ); + const uint32_t held = unpaid_blocks_of(target); + + // Register and post the whole required bond at once, while the request is still queued. + BOOST_REQUIRE_EQUAL( success(), regoperator(target) ); + BOOST_REQUIRE_EQUAL( success(), credit_collateral(target, base_min_bond) ); + BOOST_REQUIRE( OperatorStatus::OPERATOR_STATUS_UNKNOWN == operator_status(target) ); + + // Cancelling it frees the bond, and the next payout pays every held block. + BOOST_REQUIRE_EQUAL( success(), cancelwtdw(target, stale_request) ); + BOOST_REQUIRE( !withdraw_request_queued(stale_request) ); + BOOST_REQUIRE( OperatorStatus::OPERATOR_STATUS_ACTIVE == operator_status(target) ); + assert_payout_pays_held_blocks(names, target, held); +} FC_LOG_AND_RETHROW() + // --------------------------------------------------------------------------- // Position versus index slot // --------------------------------------------------------------------------- diff --git a/contracts/tests/sysio.finalizer_key_tests.cpp b/contracts/tests/sysio.finalizer_key_tests.cpp index a5a69b3611..ba3b17ebf8 100644 --- a/contracts/tests/sysio.finalizer_key_tests.cpp +++ b/contracts/tests/sysio.finalizer_key_tests.cpp @@ -116,6 +116,15 @@ BOOST_AUTO_TEST_SUITE(sysio_system_finalizer_key_tests) const name alice = "alice1111111"_n; const name bob = "bob111111111"_n; +/// Fields of the `finalizers` and `finkeys` rows. +namespace finalizer_field { +constexpr char active_key_id[] = "active_key_id"; +constexpr char finalizer_key_count[] = "finalizer_key_count"; +constexpr char active_key_binary[] = "active_key_binary"; +constexpr char finalizer_name[] = "finalizer_name"; +constexpr char finalizer_key[] = "finalizer_key"; +} // namespace finalizer_field + const std::string finalizer_key_1 = "PUB_BLS_6j4Y3LfsRiBxY-DgvqrZNMCttHftBQPIWwDiN2CMhHWULjN1nGwM1O_nEEJefqwAG4X09n4Kdt4a1mfZ1ES1cLGjQo6uLLSloiVW4i9BUhMHU2nVujP1_U_9ihdI3egZ17N-iA"; const std::string finalizer_key_2 = "PUB_BLS_gtaOjOTa0NzDt8etBDqLoZKlfKTpTalcdfmbTJknLUJB2Fu4Cv-uoa8unF3bJ5kFewaCzf3tjYUyNE6CDSrwvYP5Nw47Y9oE9x4nqJWfJykMOoaI0kJz-GDrGN2nZdUAp5tWEg"; const std::string finalizer_key_3 = "PUB_BLS_CT8khvZYYZdObeIV9aTnd8fZ8bdaCL1UpSRyqNLZZM5sdrOSpOjDTAY2drTYGvQPVS21BhtD8acLJhqGyTfjqrWjyY5FTfqLdcligofSpa2lrG3FqKVNeUULR5QgcIMYga4vkQ"; @@ -525,6 +534,35 @@ BOOST_FIXTURE_TEST_CASE(delete_last_finalizer_key_test, finalizer_key_tester) tr } FC_LOG_AND_RETHROW() // delete_last_finalizer_key_test +// A deleted key can be registered again. The new registration gets a fresh id, because key ids are +// never reused, even once no row holds the old one. +BOOST_FIXTURE_TEST_CASE(reregister_deleted_finalizer_key_gets_new_id, finalizer_key_tester) try { + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + const uint64_t first_id = get_finalizer_info(alice)[finalizer_field::active_key_id].as_uint64(); + + // Deleting alice's only key erases her finalizer row along with the key row. + BOOST_REQUIRE_EQUAL( success(), delete_finalizer_key(alice, finalizer_key_1) ); + BOOST_REQUIRE( get_finalizer_info(alice).is_null() ); + BOOST_REQUIRE( get_finalizer_key_info(first_id).is_null() ); + + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + + // The key is active again under a new id, and the old id stays vacant. + const auto alice_info = get_finalizer_info(alice); + const uint64_t second_id = alice_info[finalizer_field::active_key_id].as_uint64(); + BOOST_REQUIRE_GT( second_id, first_id ); + BOOST_REQUIRE_EQUAL( 1, alice_info[finalizer_field::finalizer_key_count].as_uint64() ); + BOOST_REQUIRE_EQUAL( finalizer_key_binary_1, alice_info[finalizer_field::active_key_binary].as_string() ); + + const auto key_info = get_finalizer_key_info(second_id); + BOOST_REQUIRE_EQUAL( alice.to_string(), key_info[finalizer_field::finalizer_name].as_string() ); + BOOST_REQUIRE_EQUAL( finalizer_key_1, key_info[finalizer_field::finalizer_key].as_string() ); + BOOST_REQUIRE( get_finalizer_key_info(first_id).is_null() ); +} +FC_LOG_AND_RETHROW() // reregister_deleted_finalizer_key_gets_new_id + // After registering keys and waiting for update_ranked_producers, test key activation BOOST_FIXTURE_TEST_CASE(multi_activation_tests, finalizer_key_tester) try { auto producer_names = activate_producers_with_operators(); diff --git a/contracts/tests/sysio.roa_tests.cpp b/contracts/tests/sysio.roa_tests.cpp index 49e64cb5a6..e3baa73524 100644 --- a/contracts/tests/sysio.roa_tests.cpp +++ b/contracts/tests/sysio.roa_tests.cpp @@ -38,6 +38,13 @@ using sysio::chain::compute_table_id; constexpr account_name ROA = "sysio.roa"_n; constexpr uint64_t NETWORK_GEN = 0; +/// Weight fields of a `policies` row. +namespace policy_field { +constexpr char net_weight[] = "net_weight"; +constexpr char cpu_weight[] = "cpu_weight"; +constexpr char ram_weight[] = "ram_weight"; +} // namespace policy_field + /// Generated sub-account names newuser checks before giving up. constexpr size_t NEWUSER_MAX_NAME_ATTEMPTS = 100; /// newuser's abort when the nonce was already used by the same creator. @@ -1502,6 +1509,43 @@ BOOST_FIXTURE_TEST_CASE( reducepolicy_wrong_symbol, sysio_roa_full_tester ) try sysio_assert_message_is("policy weights must be denominated in the core SYS symbol")); } FC_LOG_AND_RETHROW() +// ===== 1c. shared network_gen guard ===== + +// activateroa fixes the generation at 0 and no action advances it, so each policy action rejects +// generation 1 while the same call at the current generation goes through. +BOOST_FIXTURE_TEST_CASE( policy_actions_reject_future_network_gen, sysio_roa_full_tester ) try { + constexpr auto future_gen = NETWORK_GEN + 1; + constexpr auto invalid_gen = "Invalid network generation."; + constexpr auto granted = "2.0000 SYS"; // per resource, by addpolicy + constexpr auto step = "1.0000 SYS"; // per resource, added by expandpolicy and taken back by reducepolicy + constexpr uint32_t unlocked_time_block = 0; // lets reducepolicy run at once + const auto issuer = node_owners[2]; + const auto user = create_newuser(issuer); + produce_block(); + + BOOST_CHECK_EXCEPTION( + add_roa_policy(issuer, user, granted, granted, granted, unlocked_time_block, future_gen), + sysio_assert_message_exception, sysio_assert_message_is(invalid_gen)); + add_roa_policy(issuer, user, granted, granted, granted, unlocked_time_block, NETWORK_GEN); + + BOOST_CHECK_EXCEPTION( + expand_roa_policy(issuer, user, step, step, step, future_gen), + sysio_assert_message_exception, sysio_assert_message_is(invalid_gen)); + expand_roa_policy(issuer, user, step, step, step, NETWORK_GEN); + + BOOST_CHECK_EXCEPTION( + reduce_roa_policy(issuer, user, step, step, step, future_gen), + sysio_assert_message_exception, sysio_assert_message_is(invalid_gen)); + reduce_roa_policy(issuer, user, step, step, step, NETWORK_GEN); + produce_block(); + + // Only the current-generation calls moved the policy, and their expand and reduce cancel out. + const auto p = get_policy(user, issuer); + for (const char* field : {policy_field::net_weight, policy_field::cpu_weight, policy_field::ram_weight}) { + BOOST_TEST(p[field].as_string() == granted); + } +} FC_LOG_AND_RETHROW() + // ===== 2. expandpolicy validation ===== // Expand non-existent policy should fail diff --git a/docs/becoming-a-block-producer.md b/docs/becoming-a-block-producer.md index 1bea6f891c..af41eda19c 100644 --- a/docs/becoming-a-block-producer.md +++ b/docs/becoming-a-block-producer.md @@ -213,6 +213,15 @@ first payout after you are payable again. Unregistering right after producing an re-registering before your next round costs you nothing provided you still meet the admission requirements; if governance raised a collateral minimum while you were parked, top up first. +Termination does not forfeit them either, but re-registering alone does not make you payable: +termination credits your whole bond to your `claimremit` claim, and `regoperator` starts a new +`UNKNOWN` registration with no bond. Cancel any withdrawal still queued from before termination with +`cancelwtdw`: the queue is per account, so it would reserve part of the new bond and be paid out of +it when it matures. Then post every required bond again, and call `regproducer` if your producer row +was parked; the held blocks are paid at the first payout after that. Slashing is the exception: a +slashed producer can never be payable again, so any blocks it produced and was not yet paid for are +forfeited. + The one bound worth stating: a payout walks the ranking from the top and stops after a fixed number of rows, far below which no producer is paid anything anyway. Settling held blocks therefore requires you to be back within that reach, which is roughly twenty times the paid band — so in diff --git a/docs/roa-overview.md b/docs/roa-overview.md index 5dd9363aef..04ffda6655 100644 --- a/docs/roa-overview.md +++ b/docs/roa-overview.md @@ -289,7 +289,7 @@ A policy is a grant of resource weight from a node owner (the **issuer**) to an | `cpu_weight` | SYS-denominated weight granted for CPU | | `ram_weight` | SYS-denominated weight granted for RAM | | `bytes_per_unit` | Bytes per 0.0001 SYS, **frozen at the moment the policy was created** | -| `time_block` | Block height before which the policy cannot be reduced or reclaimed | +| `time_block` | Block height before which the policy cannot be reduced or reclaimed; at or below the current block, the grant is unlocked | RAM weight converts to bytes at `bytes_per_unit`. At the network's launch price of 104 bytes per 0.0001 SYS, **1 SYS of `ram_weight` ≈ 1.04 MB**. @@ -836,9 +836,10 @@ owners simultaneously, and any one of them can reduce their own policy — at or cleanly: see [Stacking policies](#stacking-policies-from-multiple-issuers) for what consumed RAM does to the reclaim. -**Grants are time-committed.** `reducepolicy` is blocked until `time_block`, and `extendpolicy` can -only push that block further out, never shorten it. An issuer commits to a term when they issue and -cannot reclaim before it elapses. +**Grants can be time-committed.** `reducepolicy` is blocked until `time_block`, and `extendpolicy` +can only push that block further out, never shorten it. An issuer that sets a future `time_block` +commits to that term and cannot reclaim before it elapses. A `time_block` at or below the current +block issues the grant unlocked, reducible immediately; `extendpolicy` can lock it later. **Caps are enforced on-chain** against the authoritative node-owner rows — 21 / 84 / 1,000 — counted in `regnodeowner` at registration time.