diff --git a/aptos-move/framework/supra-framework/sources/block.move b/aptos-move/framework/supra-framework/sources/block.move index 0628c6d444304..cedf43e8427e3 100644 --- a/aptos-move/framework/supra-framework/sources/block.move +++ b/aptos-move/framework/supra-framework/sources/block.move @@ -18,6 +18,7 @@ module supra_framework::block { use supra_framework::system_addresses; use supra_framework::timestamp; use supra_framework::transaction_fee; + use supra_framework::leader_ban_registry; friend supra_framework::genesis; @@ -216,6 +217,8 @@ module supra_framework::block { // transition is the last block in the previous epoch. stake::update_performance_statistics(proposer_index, failed_proposer_indices); state_storage::on_new_block(reconfiguration::current_epoch()); + + leader_ban_registry::update_ban_registry(epoch, round, proposer_index, failed_proposer_indices); automation_registry::monitor_cycle_end(); diff --git a/aptos-move/framework/supra-framework/sources/configs/config_buffer.move b/aptos-move/framework/supra-framework/sources/configs/config_buffer.move index 9a3d384827555..a2fa31c0cc8e8 100644 --- a/aptos-move/framework/supra-framework/sources/configs/config_buffer.move +++ b/aptos-move/framework/supra-framework/sources/configs/config_buffer.move @@ -32,6 +32,7 @@ module supra_framework::config_buffer { friend supra_framework::randomness_config_seqnum; friend supra_framework::version; friend supra_framework::automation_registry; + friend supra_framework::leader_ban_registry_config; /// Config buffer operations failed with permission denied. const ESTD_SIGNER_NEEDED: u64 = 1; diff --git a/aptos-move/framework/supra-framework/sources/configs/leader_ban_registry_config.move b/aptos-move/framework/supra-framework/sources/configs/leader_ban_registry_config.move new file mode 100644 index 0000000000000..0c1ac2066d544 --- /dev/null +++ b/aptos-move/framework/supra-framework/sources/configs/leader_ban_registry_config.move @@ -0,0 +1,266 @@ +/// Provides the config related to leader ban registry +module supra_framework::leader_ban_registry_config { + use std::error; + use std::option; + use std::option::Option; + use std::vector; + use supra_std::decode_bcs; + use supra_framework::config_buffer; + use supra_framework::system_addresses; + #[test_only] + use std::signer; + + friend supra_framework::genesis; + friend supra_framework::reconfiguration_with_dkg; + + #[test_only] + friend supra_framework::test_leader_ban_registry_config; + #[test_only] + friend supra_framework::test_leader_ban_registry; + + /// The provided on chain config bytes are empty or invalid + const EINVALID_CONFIG: u64 = 1; + /// The provided on chain config version should be equal or greater than existing + const EINVALID_VERSION: u64 = 2; + /// Decoding from version bytes failed + const EINVALID_VERSION_BYTES: u64 = 3; + /// The BanRegistryParameters already initialised + const EALREADY_INITIALISED: u64 = 4; + + /// Holds ban registry parameters bytes and it's version + struct BanRegistryParameters has drop, key, store { + /// Denotes config bcs bytes + config: vector, + /// Denotes config version + version: u8 + } + + /// Ban registry parameters v0 + struct BanRegistryParametersV0 has drop, key, store { + /// Denotes initial election count denied + initial_elections_denied: u8, + /// Denotes max election count denied + max_elections_denied: u32, + /// Denotes the minimum number of validators that must remain eligible for proposal. This + /// helps to preserve liveness in the presence of extended periods of network asynchrony. + minimum_unbanned_proposers: u8, + /// Denotes the number of elections a validator must serve on probation after ban expires. + /// The ban duration compounds each time a validator is banned whilst on probation, and + /// resets to the base duration if the validator passes probation. + probation_elections: u8 + } + + /// Publishes the BanRegistryParameters config. + public(friend) fun initialize( + supra_framework: &signer, config: vector + ) { + system_addresses::assert_supra_framework(supra_framework); + assert!(vector::length(&config) != 0, error::invalid_argument(EINVALID_CONFIG)); + assert!( + !exists(@supra_framework), + error::already_exists(EALREADY_INITIALISED) + ); + let v0 = deserialise_v0_params(config); + if (option::is_none(&v0)) { + abort error::invalid_argument(EINVALID_VERSION_BYTES) + }; + // we always init with version 0 + move_to(supra_framework, BanRegistryParameters { config, version: 0 }); + let v0_params = option::extract(&mut v0); + move_to(supra_framework, v0_params); + } + + /// This can be called by on-chain governance to update on-chain configs for the next epoch. + /// Example usage: + /// ``` + /// supra_framework::leader_ban_registry_config::set_for_next_epoch(&framework_signer, some_config_bytes, version); + /// supra_framework::supra_governance::reconfigure(&framework_signer); + /// ``` + public fun set_for_next_epoch( + account: &signer, config: vector, version: u8 + ) acquires BanRegistryParameters { + system_addresses::assert_supra_framework(account); + assert!(vector::length(&config) != 0, error::invalid_argument(EINVALID_CONFIG)); + if (exists(@supra_framework)) { + let ban_registry_params = + borrow_global(@supra_framework); + assert!( + version >= ban_registry_params.version, + error::invalid_argument(EINVALID_VERSION) + ); + }; + std::config_buffer::upsert( + BanRegistryParameters { config, version } + ); + } + + /// Only used in reconfigurations to apply the pending `BanRegistryParameters`, if there is any. + public(friend) fun on_new_epoch( + framework: &signer + ) acquires BanRegistryParameters, BanRegistryParametersV0 { + system_addresses::assert_supra_framework(framework); + if (config_buffer::does_exist()) { + let new_config = config_buffer::extract(); + if (exists(@supra_framework)) { + *borrow_global_mut(@supra_framework) = new_config; + } else { + move_to(framework, new_config); + }; + let params = borrow_global(@supra_framework); + if (params.version == 0) { + let v0 = deserialise_v0_params(params.config); + // This is to prevent abort on new epoch if deserialise failed. + if (option::is_some(&v0)) { + let ban_registry_params = option::extract(&mut v0); + if (exists(@supra_framework)) { + *borrow_global_mut(@supra_framework) = ban_registry_params; + } else { + move_to(framework, ban_registry_params); + } + } + } + // later version can be assigned here + } + } + + #[view] + public fun get_ban_registry_params(): (vector, u8) acquires BanRegistryParameters { + if (exists(@supra_framework)) { + let ban_registry_config = + borrow_global(@supra_framework); + return (ban_registry_config.config, ban_registry_config.version) + }; + (vector::empty(), 0) + } + + #[view] + public fun get_ban_registry_params_v0(): (u8, u32, u8, u8) acquires BanRegistryParameters, BanRegistryParametersV0 { + if (exists(@supra_framework)) { + let ban_registry_config = + borrow_global(@supra_framework); + if (ban_registry_config.version == 0) { + let ban_registry_params = + borrow_global(@supra_framework); + return ( + ban_registry_params.initial_elections_denied, + ban_registry_params.max_elections_denied, + ban_registry_params.minimum_unbanned_proposers, + ban_registry_params.probation_elections + ) + } + }; + (0, 0, 0, 0) + } + + /// Provide initial election denied value + public fun get_initial_elections_denied(): u8 acquires BanRegistryParameters, BanRegistryParametersV0 { + if (exists(@supra_framework)) { + let ban_registry_config = + borrow_global(@supra_framework); + if (ban_registry_config.version == 0) { + let ban_registry_params = + borrow_global(@supra_framework); + return ban_registry_params.initial_elections_denied + } + }; + 0 + } + + /// Provide max election denied value + public fun get_max_elections_denied(): u32 acquires BanRegistryParameters, BanRegistryParametersV0 { + if (exists(@supra_framework)) { + let ban_registry_config = + borrow_global(@supra_framework); + if (ban_registry_config.version == 0) { + let ban_registry_params = + borrow_global(@supra_framework); + return ban_registry_params.max_elections_denied + } + }; + 0 + } + + /// Provide minimum unbanned proposers value + public fun get_minimum_unbanned_proposers(): u8 acquires BanRegistryParameters, BanRegistryParametersV0 { + if (exists(@supra_framework)) { + let ban_registry_config = + borrow_global(@supra_framework); + if (ban_registry_config.version == 0) { + let ban_registry_params = + borrow_global(@supra_framework); + return ban_registry_params.minimum_unbanned_proposers + } + }; + 0 + } + + /// Provide probation elections value + public fun get_probation_elections(): u8 acquires BanRegistryParameters, BanRegistryParametersV0 { + if (exists(@supra_framework)) { + let ban_registry_config = + borrow_global(@supra_framework); + if (ban_registry_config.version == 0) { + let ban_registry_params = + borrow_global(@supra_framework); + return ban_registry_params.probation_elections + } + }; + 0 + } + + /// Decoding bytes to `BanRegistryParametersV0` using bcs + fun deserialise_v0_params(bytes: vector): Option { + let bcs_bytes = decode_bcs::new(bytes); + let initial_elections_denied: u8 = decode_bcs::peel_u8(&mut bcs_bytes); + let max_elections_denied: u32 = decode_bcs::peel_u32(&mut bcs_bytes); + let minimum_unbanned_proposers: u8 = decode_bcs::peel_u8(&mut bcs_bytes); + let probation_elections: u8 = decode_bcs::peel_u8(&mut bcs_bytes); + // making sure no bytes left to decode means correct parameter version + if (vector::length(&decode_bcs::into_remainder_bytes(bcs_bytes)) == 0) { + return option::some( + BanRegistryParametersV0 { + initial_elections_denied, + max_elections_denied, + minimum_unbanned_proposers, + probation_elections + } + ) + }; + option::none() + } + + #[test_only] + public fun get_test_ban_registry_params_v0(): BanRegistryParametersV0 { + BanRegistryParametersV0 { + initial_elections_denied: 1, + max_elections_denied: 5, + minimum_unbanned_proposers: 2, + probation_elections: 1 + } + } + + #[test_only] + public fun get_custom_ban_registry_params_v0( + initial_elections_denied: u8, + max_elections_denied: u32, + minimum_unbanned_proposers: u8, + probation_elections: u8 + ): BanRegistryParametersV0 { + BanRegistryParametersV0 { + initial_elections_denied, + max_elections_denied, + minimum_unbanned_proposers, + probation_elections + } + } + + #[test_only] + public fun check_ban_registry_params_exist(sender: &signer): bool { + exists(signer::address_of(sender)) + } + + #[test_only] + public fun check_ban_registry_params_v0_exist(sender: &signer): bool { + exists(signer::address_of(sender)) + } +} diff --git a/aptos-move/framework/supra-framework/sources/genesis.move b/aptos-move/framework/supra-framework/sources/genesis.move index a13f985ba3823..a56f60f0a0d4c 100644 --- a/aptos-move/framework/supra-framework/sources/genesis.move +++ b/aptos-move/framework/supra-framework/sources/genesis.move @@ -19,6 +19,8 @@ module supra_framework::genesis { use supra_framework::evm_genesis_config; use supra_framework::create_signer::create_signer; use supra_framework::gas_schedule; + use supra_framework::leader_ban_registry; + use supra_framework::leader_ban_registry_config; use supra_framework::multisig_account; use supra_framework::pbo_delegation_pool; use supra_framework::reconfiguration; @@ -291,6 +293,15 @@ module supra_framework::genesis { evm_genesis_config::initialize(supra_framework, evm_genesis_config); } + /// Initialize the leader ban config + fun initialize_leader_ban_registry_config( + supra_framework: &signer, + leader_ban_registry_config: vector, + ) { + leader_ban_registry_config::initialize(supra_framework, leader_ban_registry_config); + leader_ban_registry::initialize_leader_ban_registry(supra_framework); + } + fun create_accounts(supra_framework: &signer, accounts: vector) { let unique_accounts = vector::empty(); diff --git a/aptos-move/framework/supra-framework/sources/leader_ban_registry.move b/aptos-move/framework/supra-framework/sources/leader_ban_registry.move new file mode 100644 index 0000000000000..a04dcb4a46a8d --- /dev/null +++ b/aptos-move/framework/supra-framework/sources/leader_ban_registry.move @@ -0,0 +1,586 @@ +/// Maintains the list of banned validators and updates counters on every epoch. +/// +/// This implementation assumes that each validator is elected once every `n` consensus rounds on expectation, +/// where `n` is the number of validators in the consensus committee (i.e. the `ValidatorSet`). +module supra_framework::leader_ban_registry { + use std::error; + use std::features; + use std::option; + use std::option::Option; + use supra_framework::system_addresses; + use std::vector; + use aptos_std::math64::{pow, min}; + use supra_framework::event; + use supra_framework::stake; + use supra_framework::leader_ban_registry_config; + + friend supra_framework::block; + friend supra_framework::genesis; + friend supra_framework::reconfiguration; + + #[test_only] + friend supra_framework::test_leader_ban_registry; + + /// Leader ban registry already initialized + const EBAN_REGISTRY_ALREADY_EXISTS: u64 = 1; + /// Leader ban registry not initialized + const EBAN_REGISTRY_NOT_INITIALIZED: u64 = 2; + /// Latest view already initialized + const ELATEST_VIEW_ALREADY_EXISTS: u64 = 3; + + /// Information about a ban that is currently in effect for a validator. + struct ActiveBan has store, drop, copy { + /// The consensus epoch in which the current ban was issued (if `on_probation` is `false`) or + /// when its probation period started (if `on_probation` is `true`). + epoch_earned: u64, + /// The consensus round in which the current ban was issued (if `on_probation` is `false`) or + /// when its probation period started (if `on_probation` is `true`). + round_earned: u64, + /// Round count incremented on every epoch change + rounds_served_in_previous_epochs: u64, + /// If `true` then the current ban period has expired and the validator is currently on probation; i.e., it is + /// eligible for election but will be banned for a longer period if it once again fails to propose a canonical + /// block when elected. If `true` then the other fields of this struct denote the consensus view in which + /// the probation period started. + on_probation: bool + } + + /// Holds validator metrics regarding duration pool address etc + struct ValidatorBans has store, drop, copy { + /// Information about the ban that is currently in effect. + active: ActiveBan, + /// The number of consecutive probations that this validator has failed. + consecutive_bans: u32, + /// Validator's pool address + pool_address: address + } + + /// Holds ban registry + struct BanRegistry has drop, store, key { + /// List of validator active bans with pool address + bans: vector + } + + /// Holds latest processed round and epoch + struct LatestView has drop, store, key, copy { + /// Epoch + epoch: u64, + /// Round + round: u64 + } + + #[event] + /// Emits when validator receives a ban or consucutive ban occurred + struct Banned has drop, store { + /// Validator's pool address + pool_address: address, + /// Epoch + epoch: u64, + /// Round + round: u64, + /// The number of consecutive probations that this validator has failed. + consecutive_bans: u32 + } + + #[event] + /// Emitted when a validator's ban is lifted and its probation period starts. A validator that is on probation is + /// eligible for election again, but will be banned for longer if it once again fails to propose a canonical block. + struct ReinstatedWithProbation has drop, store { + /// Validator's pool address + pool_address: address, + /// Epoch + epoch: u64, + /// Round + round: u64 + } + + #[event] + /// Emitted when a validator's probation period ends without the validator earning a new ban. + struct Reinstated has drop, store { + /// Validator's pool address + pool_address: address, + /// Epoch + epoch: u64, + /// Round + round: u64 + } + + /// Initialise leader ban registry + public(friend) fun initialize_leader_ban_registry( + supra_framework: &signer + ) { + system_addresses::assert_supra_framework(supra_framework); + assert!( + !exists(@supra_framework), + error::already_exists(EBAN_REGISTRY_ALREADY_EXISTS) + ); + assert!( + !exists(@supra_framework), + error::already_exists(EBAN_REGISTRY_ALREADY_EXISTS) + ); + move_to(supra_framework, BanRegistry { bans: vector::empty() }); + move_to(supra_framework, LatestView { epoch: 0, round: 0 }); + } + + #[view] + /// Returns list of validators active ban with it's pool address + public fun get_ban_registry(): vector acquires BanRegistry { + if (!exists(@supra_framework)) { + return vector::empty() + }; + let ban_registry = borrow_global(@supra_framework); + ban_registry.bans + } + + #[view] + /// Return latest view + public fun get_latest_view(): LatestView acquires LatestView { + if (!exists(@supra_framework)) { + return LatestView { epoch: 0, round: 0 } + }; + let latest_view = borrow_global(@supra_framework); + *latest_view + } + + #[view] + /// Returns the number of consensus rounds that a validator is banned for when it fails to propose a + /// canonical block when elected as leader whilst not on probation. + public fun get_initial_ban_duration(): u64 { + let initial_elections_denied = + leader_ban_registry_config::get_initial_elections_denied(); + let committee_size = stake::get_committee_size(); + committee_size * (initial_elections_denied as u64) + } + + #[view] + /// Returns the maximum number of consensus rounds that a validator may banned for when it repeatedly fails to + /// propose a canonical block when elected as leader whilst on probation. + public fun get_max_ban_duration(): u64 { + let max_elections_denied = leader_ban_registry_config::get_max_elections_denied(); + let committee_size = stake::get_committee_size(); + committee_size * (max_elections_denied as u64) + } + + #[view] + /// Returns the number of consensus rounds that a validator is considered to be on probation for after having + /// served its most recent ban. + public fun get_probation_duration(): u64 { + let probation_elections = leader_ban_registry_config::get_probation_elections(); + let committee_size = stake::get_committee_size(); + committee_size * (probation_elections as u64) + } + + #[view] + /// Returns the number of consensus rounds remaining in the ban for the validator with the given + /// pool address. Returns 0 if the validator is not banned (including if it is on probation). + public fun get_remaining_ban_duration(pool_address: address): u64 acquires BanRegistry, LatestView { + if (!exists(@supra_framework) || !exists(@supra_framework)) { + return 0 + }; + let ban_registry = borrow_global(@supra_framework); + let latest_view = borrow_global(@supra_framework); + let (found, index) = vector::find( + &ban_registry.bans, + |v| { + let v: &ValidatorBans = v; + v.pool_address == pool_address && !v.active.on_probation + } + ); + if (found) { + remaining_ban_duration(vector::borrow(&ban_registry.bans, index), latest_view) + } else { + 0 + } + } + + #[view] + /// Returns the number of consensus rounds remaining in the probation period for the validator + /// with the given pool address. Returns 0 if the validator is not on probation. + public fun get_remaining_probation_duration(pool_address: address): u64 acquires BanRegistry, LatestView { + if (!exists(@supra_framework) || !exists(@supra_framework)) { + return 0 + }; + let ban_registry = borrow_global(@supra_framework); + let latest_view = borrow_global(@supra_framework); + let (found, index) = vector::find( + &ban_registry.bans, + |v| { + let v: &ValidatorBans = v; + v.pool_address == pool_address && v.active.on_probation + } + ); + if (found) { + let probation_dur = get_probation_duration(); + remaining_probation_duration(vector::borrow(&ban_registry.bans, index), latest_view, probation_dur) + } else { + 0 + } + } + + /// Add or update the ban registry as per block metadata + public(friend) fun update_ban_registry( + current_epoch: u64, + current_round: u64, + proposer_index: Option, + failed_proposer_indices: vector + ) acquires BanRegistry, LatestView { + if (!exists(@supra_framework)) { return }; + if (!exists(@supra_framework)) { return }; + let ban_registry = borrow_global_mut(@supra_framework); + let latest_view = borrow_global_mut(@supra_framework); + latest_view.epoch = current_epoch; + latest_view.round = current_round; + + // ban the failed proposers + ban_failed_proposers(latest_view, failed_proposer_indices, ban_registry); + + // remove expired bans + reinstate_expired_bans(latest_view, ban_registry); + } + + /// Adds failed proposer indices to ban registry + fun ban_failed_proposers( + latest_view: &LatestView, + failed_proposer_indices: vector, + ban_registry: &mut BanRegistry + ) { + let initial_ban_duration = get_initial_ban_duration(); + if (initial_ban_duration == 0) { return }; + + vector::for_each( + failed_proposer_indices, + |failed_validator_index| { + let validator_pool_address_opt = + stake::get_pool_address_from_index(failed_validator_index); + + if (option::is_some(&validator_pool_address_opt)) { + let validator_pool_address = + option::extract(&mut validator_pool_address_opt); + let (is_banned, index) = vector::find( + &ban_registry.bans, + |v| { + let v: &ValidatorBans = v; + validator_pool_address == v.pool_address + } + ); + if (is_banned) { + // Validator is already in registry (either banned or on probation). + // Re-banning resets the ban period and increases consecutive count. + // If the consensus code is implemented correctly then the validator should + // not be re-banned whilst serving a ban as it should not be eligible for election + // when banned (i.e. this branch should only be taken when a validator is on probation). + let bans = vector::borrow_mut(&mut ban_registry.bans, index); + bans.consecutive_bans = bans.consecutive_bans + 1; + bans.active.round_earned = latest_view.round; + bans.active.epoch_earned = latest_view.epoch; + bans.active.rounds_served_in_previous_epochs = 0; + bans.active.on_probation = false; // Reset to banned state + + if (features::module_event_enabled()) { + event::emit( + Banned { + pool_address: validator_pool_address, + epoch: latest_view.epoch, + round: latest_view.round, + consecutive_bans: bans.consecutive_bans + } + ); + } + } else { + let ban_registry_len = vector::length(&ban_registry.bans); + if (can_be_banned(ban_registry_len)) { + let ban_with_address = ValidatorBans { + active: ActiveBan { + epoch_earned: latest_view.epoch, + round_earned: latest_view.round, + rounds_served_in_previous_epochs: 0, + on_probation: false + }, + consecutive_bans: 0, + pool_address: validator_pool_address + }; + vector::push_back(&mut ban_registry.bans, ban_with_address); + + if (features::module_event_enabled()) { + event::emit( + Banned { + pool_address: ban_with_address.pool_address, + epoch: ban_with_address.active.epoch_earned, + round: ban_with_address.active.round_earned, + consecutive_bans: ban_with_address.consecutive_bans + } + ); + } + } + }; + }; + } + ); + } + + /// Handles ban and probation expiry: + /// - When probation expires: removes from registry, emits Reinstated + /// - When ban expires and probation_duration > 0: transitions to probation, emits ReinstatedWithProbation + /// - When ban expires and probation_duration == 0: removes from registry, emits Reinstated + fun reinstate_expired_bans( + latest_view: &LatestView, ban_registry: &mut BanRegistry + ) { + let probation_duration = get_probation_duration(); + + // First pass: remove validators whose probation has expired. + // Done before ban-to-probation transitions to avoid iterating just-transitioned validators. + // Always runs (even when probation_duration == 0) to clean up validators that were already + // on probation before a config change set probation_duration to 0. + let pool_addresses_for_full_reinstatement = vector::empty(); + vector::for_each_ref( + &ban_registry.bans, + |v| { + let v: &ValidatorBans = v; + if (v.active.on_probation + && remaining_probation_duration(v, latest_view, probation_duration) == 0) { + vector::push_back( + &mut pool_addresses_for_full_reinstatement, v.pool_address + ); + } + } + ); + + vector::for_each_ref( + &pool_addresses_for_full_reinstatement, + |p| { + let (found, index) = vector::find( + &ban_registry.bans, + |v| { + let v: &ValidatorBans = v; + &v.pool_address == p + } + ); + if (found) { + vector::swap_remove(&mut ban_registry.bans, index); + + if (features::module_event_enabled()) { + event::emit( + Reinstated { + epoch: latest_view.epoch, + round: latest_view.round, + pool_address: *p + } + ) + } + } + } + ); + + // Second pass: handle expired bans + let pool_addresses_with_expired_bans = vector::empty(); + vector::for_each_ref( + &ban_registry.bans, + |v| { + let v: &ValidatorBans = v; + if (!v.active.on_probation && remaining_ban_duration(v, latest_view) == 0) { + vector::push_back(&mut pool_addresses_with_expired_bans, v.pool_address); + } + } + ); + + if (probation_duration > 0) { + // Transition expired bans to probation + vector::for_each_ref( + &pool_addresses_with_expired_bans, + |p| { + let (found, index) = vector::find( + &ban_registry.bans, + |v| { + let v: &ValidatorBans = v; + &v.pool_address == p + } + ); + if (found) { + let ban = vector::borrow_mut(&mut ban_registry.bans, index); + ban.active.on_probation = true; + // Reset active fields so probation duration is calculated from this point + ban.active.epoch_earned = latest_view.epoch; + ban.active.round_earned = latest_view.round; + ban.active.rounds_served_in_previous_epochs = 0; + + if (features::module_event_enabled()) { + event::emit( + ReinstatedWithProbation { + epoch: latest_view.epoch, + round: latest_view.round, + pool_address: *p + } + ) + } + } + } + ); + } else { + // No probation period - directly remove validators whose ban expired + vector::for_each_ref( + &pool_addresses_with_expired_bans, + |p| { + let (found, index) = vector::find( + &ban_registry.bans, + |v| { + let v: &ValidatorBans = v; + &v.pool_address == p + } + ); + if (found) { + vector::swap_remove(&mut ban_registry.bans, index); + + if (features::module_event_enabled()) { + event::emit( + Reinstated { + epoch: latest_view.epoch, + round: latest_view.round, + pool_address: *p + } + ) + } + } + } + ); + }; + } + + /// Increments the total number of consensus rounds served by each banned validator and removes + /// registry entries for validators that have left the validator set. + /// + /// The number of rounds in each epoch may vary due to network asynchrony, so we must record the + /// number of rounds served in previous epochs to be able to ensure that a banned validator serves its + /// full ban period when its ban span multiple epochs. + public(friend) fun on_new_epoch() acquires BanRegistry, LatestView { + if (!exists(@supra_framework)) { return }; + if (!exists(@supra_framework)) { return }; + let latest_view = borrow_global(@supra_framework); + let ban_registry = borrow_global_mut(@supra_framework); + + // The pool addresses of the validators for the new epoch. + let new_committee_pool_addresses = stake::get_committee_pool_addresses(); + // The pool addresses of the validators that have left the committee. + let retired_validators = vector::empty(); + vector::for_each_mut( + &mut ban_registry.bans, + |v| { + let v: &mut ValidatorBans = v; + if (vector::contains(&new_committee_pool_addresses, &v.pool_address)) { + if (latest_view.epoch > v.active.epoch_earned) { + v.active.rounds_served_in_previous_epochs = v.active.rounds_served_in_previous_epochs + + latest_view.round; + } else if (latest_view.epoch == v.active.epoch_earned && latest_view.round > v.active.round_earned) { + v.active.rounds_served_in_previous_epochs = latest_view.round - v.active.round_earned; + }; + // else: The ban hasn't started yet. + } else { + vector::push_back(&mut retired_validators, v.pool_address) + } + } + ); + + vector::for_each_ref( + &retired_validators, + |p| { + let (is_banned, index) = vector::find( + &ban_registry.bans, + |v| { + let v: &ValidatorBans = v; + &v.pool_address == p + } + ); + if (is_banned) { + vector::swap_remove(&mut ban_registry.bans, index); + + if (features::module_event_enabled()) { + event::emit( + Reinstated { + pool_address: *p, + epoch: latest_view.epoch, + round: latest_view.round + } + ); + } + } + } + ); + } + + /// Calculate the number of rounds remaining in a given ban (not including probation). + fun remaining_ban_duration( + ban: &ValidatorBans, latest_view: &LatestView + ): u64 { + let initial_ban_duration = get_initial_ban_duration(); + let max_ban_duration = get_max_ban_duration(); + let duration = initial_ban_duration * pow(2, (ban.consecutive_bans as u64)); + let duration = min(duration, max_ban_duration); + let rounds_served = + if (latest_view.epoch > ban.active.epoch_earned) { + ban.active.rounds_served_in_previous_epochs + latest_view.round + } else if (latest_view.epoch == ban.active.epoch_earned && latest_view.round > ban.active.round_earned) { + latest_view.round - ban.active.round_earned + } else { + // The ban hasn't started yet. + 0 + }; + if (duration > rounds_served) { + duration - rounds_served + } else { 0 } + } + + /// Calculate the number of rounds remaining in probation. + /// Probation duration is constant and does not scale with consecutive bans. + fun remaining_probation_duration( + ban: &ValidatorBans, latest_view: &LatestView, probation_duration: u64 + ): u64 { + let rounds_served = + if (latest_view.epoch > ban.active.epoch_earned) { + ban.active.rounds_served_in_previous_epochs + latest_view.round + } else if (latest_view.epoch == ban.active.epoch_earned && latest_view.round > ban.active.round_earned) { + latest_view.round - ban.active.round_earned + } else { + // The ban hasn't started yet. + 0 + }; + if (probation_duration > rounds_served) { + probation_duration - rounds_served + } else { 0 } + } + + /// Returns true until ban registry size + minimum proposers required count less than committee size + fun can_be_banned(ban_registry_len: u64): bool { + let minimum_unbanned_proposers = + leader_ban_registry_config::get_minimum_unbanned_proposers(); + let committee_size = stake::get_committee_size(); + committee_size > ban_registry_len + (minimum_unbanned_proposers as u64) + } + + /// Validates registry initialised if not aborted with `EBAN_REGISTRY_NOT_INITIALIZED` + fun assert_registry_initialized() { + assert!( + exists(@supra_framework), + error::invalid_state(EBAN_REGISTRY_NOT_INITIALIZED) + ); + } + + #[test_only] + public fun get_pool_address_from_vp( + validator_with_pool_addr: &ValidatorBans + ): address { + validator_with_pool_addr.pool_address + } + + #[test_only] + public fun get_consecutive_count_from_vp( + validator_with_pool_addr: &ValidatorBans + ): u32 { + validator_with_pool_addr.consecutive_bans + } + + #[test_only] + public fun is_on_probation_from_vp( + validator_with_pool_addr: &ValidatorBans + ): bool { + validator_with_pool_addr.active.on_probation + } +} diff --git a/aptos-move/framework/supra-framework/sources/reconfiguration.move b/aptos-move/framework/supra-framework/sources/reconfiguration.move index 4e082ad8fac3c..fe46773ee1ef3 100644 --- a/aptos-move/framework/supra-framework/sources/reconfiguration.move +++ b/aptos-move/framework/supra-framework/sources/reconfiguration.move @@ -4,6 +4,7 @@ module supra_framework::reconfiguration { use std::error; use std::features; use std::signer; + use supra_framework::leader_ban_registry; use supra_framework::account; use supra_framework::chain_status; @@ -151,6 +152,7 @@ module supra_framework::reconfiguration { // Call stake to compute the new validator set and distribute rewards and transaction fees. stake::on_new_epoch(); + leader_ban_registry::on_new_epoch(); storage_gas::on_reconfig(); assert!(current_time > config_ref.last_reconfiguration_time, error::invalid_state(EINVALID_BLOCK_TIME)); diff --git a/aptos-move/framework/supra-framework/sources/reconfiguration_with_dkg.move b/aptos-move/framework/supra-framework/sources/reconfiguration_with_dkg.move index 08b0792ce8501..8386cf18dd9b3 100644 --- a/aptos-move/framework/supra-framework/sources/reconfiguration_with_dkg.move +++ b/aptos-move/framework/supra-framework/sources/reconfiguration_with_dkg.move @@ -10,6 +10,7 @@ module supra_framework::reconfiguration_with_dkg { use supra_framework::jwk_consensus_config; use supra_framework::jwks; use supra_framework::keyless_account; + use supra_framework::leader_ban_registry_config; use supra_framework::randomness_api_v0_config; use supra_framework::randomness_config; use supra_framework::randomness_config_seqnum; @@ -60,6 +61,7 @@ module supra_framework::reconfiguration_with_dkg { jwk_consensus_config::on_new_epoch(framework); jwks::on_new_epoch(framework); keyless_account::on_new_epoch(framework); + leader_ban_registry_config::on_new_epoch(framework); randomness_config_seqnum::on_new_epoch(framework); randomness_config::on_new_epoch(framework); randomness_api_v0_config::on_new_epoch(framework); diff --git a/aptos-move/framework/supra-framework/sources/stake.move b/aptos-move/framework/supra-framework/sources/stake.move index 4b6d9451c2205..5717ce4b2703f 100644 --- a/aptos-move/framework/supra-framework/sources/stake.move +++ b/aptos-move/framework/supra-framework/sources/stake.move @@ -40,6 +40,10 @@ module supra_framework::stake { friend supra_framework::reconfiguration; friend supra_framework::reconfiguration_with_dkg; friend supra_framework::transaction_fee; + friend supra_framework::leader_ban_registry; + + #[test_only] + friend supra_framework::test_leader_ban_registry; /// Validator Config not published. const EVALIDATOR_CONFIG: u64 = 1; @@ -506,6 +510,66 @@ module supra_framework::stake { move_to(supra_framework, SupraCoinCapabilities { mint_cap }) } + /// To get validator pool address from validator index + public(friend) fun get_pool_address_from_index(index: u64) : Option
+ acquires ValidatorSet + { + if (exists(@supra_framework)) { + let validator_set = borrow_global(@supra_framework); + // it can happen that some validator may added a leave request in between + // this can change the order of indexes in active validator + // so need to iterate through all until find pool address in active and pending_active + let (is_index_exist, i) = vector::find(&validator_set.active_validators, |v| { + let v: &ValidatorInfo = v; + v.config.validator_index == index + }); + if (is_index_exist) { + let v_info = vector::borrow(&validator_set.active_validators, i); + return option::some(v_info.addr) + }; + let (is_index_exist, i) = vector::find(&validator_set.pending_inactive, |v| { + let v: &ValidatorInfo = v; + v.config.validator_index == index + }); + if (is_index_exist) { + let v_info = vector::borrow(&validator_set.pending_inactive, i); + return option::some(v_info.addr) + }; + }; + option::none() + } + + /// Returns committee size + public(friend) fun get_committee_size() : u64 + acquires ValidatorSet + { + let commitee_size= 0; + if (exists(@supra_framework)) { + let validator_set = borrow_global(@supra_framework); + commitee_size = vector::length(&validator_set.active_validators) + vector::length(&validator_set.pending_inactive); + }; + commitee_size + } + + /// Returns pool addresses of current committee including pending inactive + public(friend) fun get_committee_pool_addresses() : vector
+ acquires ValidatorSet + { + let pool_addresses= vector::empty(); + if (exists(@supra_framework)) { + let validator_set = borrow_global(@supra_framework); + vector::for_each_ref(&validator_set.active_validators, |validator_info| { + let validator_info: &ValidatorInfo = validator_info; + vector::push_back(&mut pool_addresses, validator_info.addr); + }); + vector::for_each_ref(&validator_set.pending_inactive, |validator_info| { + let validator_info: &ValidatorInfo = validator_info; + vector::push_back(&mut pool_addresses, validator_info.addr); + }); + }; + pool_addresses + } + /// Allow on chain governance to remove validators from the validator set. public fun remove_validators( supra_framework: &signer, diff --git a/aptos-move/framework/supra-framework/tests/test_leader_ban_registry.move b/aptos-move/framework/supra-framework/tests/test_leader_ban_registry.move new file mode 100644 index 0000000000000..122871d34aa96 --- /dev/null +++ b/aptos-move/framework/supra-framework/tests/test_leader_ban_registry.move @@ -0,0 +1,360 @@ +#[test_only] +module std::test_leader_ban_registry { + use std::bcs; + use std::option; + use std::vector; + use aptos_std::ed25519; + use supra_framework::supra_coin; + use supra_framework::coin; + use supra_framework::supra_coin::SupraCoin; + use supra_framework::stake; + use supra_framework::leader_ban_registry; + use supra_framework::leader_ban_registry_config; + use std::signer; + use supra_framework::account; + + fun setup_staking_modules( + sender: &signer, + validator_1: &signer, + validator_2: &signer, + validator_3: &signer, + validator_4: &signer + ) { + // initialise stake module + let (_v_1_s_key, v_1_p_key) = ed25519::generate_keys(); + let (_v_2_s_key, v_2_p_key) = ed25519::generate_keys(); + let (_v_3_s_key, v_3_p_key) = ed25519::generate_keys(); + let (_v_4_s_key, v_4_p_key) = ed25519::generate_keys(); + + stake::initialize_for_test(sender); + account::create_account_for_test(signer::address_of(validator_1)); + coin::register(validator_1); + supra_coin::mint(sender, signer::address_of(validator_1), 10000000000); + + account::create_account_for_test(signer::address_of(validator_2)); + coin::register(validator_2); + supra_coin::mint(sender, signer::address_of(validator_2), 10000000000); + + account::create_account_for_test(signer::address_of(validator_3)); + coin::register(validator_3); + supra_coin::mint(sender, signer::address_of(validator_3), 10000000000); + + account::create_account_for_test(signer::address_of(validator_4)); + coin::register(validator_4); + supra_coin::mint(sender, signer::address_of(validator_4), 10000000000); + + let active_coin = coin::withdraw(validator_1, 500); + let pending_coin = coin::withdraw(validator_1, 1000); + stake::create_stake_pool( + validator_1, + active_coin, + pending_coin, + 500000000 + ); + + let active_coin = coin::withdraw(validator_2, 500); + let pending_coin = coin::withdraw(validator_2, 1000); + stake::create_stake_pool( + validator_2, + active_coin, + pending_coin, + 500000000 + ); + + let active_coin = coin::withdraw(validator_3, 500); + let pending_coin = coin::withdraw(validator_3, 1000); + stake::create_stake_pool( + validator_3, + active_coin, + pending_coin, + 500000000 + ); + + let active_coin = coin::withdraw(validator_4, 500); + let pending_coin = coin::withdraw(validator_4, 1000); + stake::create_stake_pool( + validator_4, + active_coin, + pending_coin, + 500000000 + ); + + stake::join_validator_set_for_test( + &ed25519::public_key_to_unvalidated(&v_4_p_key), + validator_4, + signer::address_of(validator_4), + false + ); + stake::join_validator_set_for_test( + &ed25519::public_key_to_unvalidated(&v_3_p_key), + validator_3, + signer::address_of(validator_3), + false + ); + stake::join_validator_set_for_test( + &ed25519::public_key_to_unvalidated(&v_2_p_key), + validator_2, + signer::address_of(validator_2), + false + ); + stake::join_validator_set_for_test( + &ed25519::public_key_to_unvalidated(&v_1_p_key), + validator_1, + signer::address_of(validator_1), + true + ); // on epoch change + } + + #[ + test( + sender = @supra_framework, + validator_1 = @0xdead01, + validator_2 = @0xdead02, + validator_3 = @0xdead03, + validator_4 = @0xdead04 + ) + ] + fun test_ban_registy_e2e( + sender: &signer, + validator_1: &signer, + validator_2: &signer, + validator_3: &signer, + validator_4: &signer + ) { + // initialise ban registry config + let ban_params = leader_ban_registry_config::get_test_ban_registry_params_v0(); + let ban_config_bytes = bcs::to_bytes(&ban_params); + leader_ban_registry_config::initialize(sender, ban_config_bytes); + + // initialise ban registry + leader_ban_registry::initialize_leader_ban_registry(sender); + setup_staking_modules( + sender, + validator_1, + validator_2, + validator_3, + validator_4 + ); + + // ===== Part 1: Basic ban -> probation -> reinstatement ===== + // Config: initial_ban_duration=4, probation_duration=4, max_ban_duration=20, committee_size=4 + + // Round 0: No failures, registry empty + leader_ban_registry::update_ban_registry(0, 0, option::some(0), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 0, 1); + + // Round 1: Validator 2 (index 1) fails to propose -> banned + leader_ban_registry::update_ban_registry(0, 1, option::some(2), vector[1]); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 1, 2); + assert!( + signer::address_of(validator_2) + == leader_ban_registry::get_pool_address_from_vp( + vector::borrow(&ban_registry, 0) + ), + 3 + ); + + // Round 4: Ban still active (duration=4, rounds_served=3, remaining=1) + leader_ban_registry::update_ban_registry(0, 4, option::some(2), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 1, 4); + let vp = vector::borrow(&ban_registry, 0); + assert!(!leader_ban_registry::is_on_probation_from_vp(vp), 100); + + // Round 5: Ban expires (rounds_served=4), transitions to probation + leader_ban_registry::update_ban_registry(0, 5, option::some(2), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 1, 5); // Still in registry (on probation) + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::is_on_probation_from_vp(vp), 101); + + // Round 9: Probation expires (rounds_served=4), fully reinstated + leader_ban_registry::update_ban_registry(0, 9, option::some(2), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 0, 6); + + // ===== Part 2: Consecutive bans via re-ban during probation ===== + + // Round 12: Validator 1 (index 0) fails -> banned, consecutive=0, duration=4 + leader_ban_registry::update_ban_registry(0, 12, option::some(1), vector[0]); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!( + signer::address_of(validator_1) + == leader_ban_registry::get_pool_address_from_vp(vp), + 7 + ); + assert!(leader_ban_registry::get_consecutive_count_from_vp(vp) == 0, 8); + + // Round 16: Ban expires (duration=4) -> probation + leader_ban_registry::update_ban_registry(0, 16, option::some(1), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::is_on_probation_from_vp(vp), 102); + + // Round 17: Re-ban during probation -> consecutive=1, duration=8 + leader_ban_registry::update_ban_registry(0, 17, option::some(1), vector[0]); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::get_consecutive_count_from_vp(vp) == 1, 9); + assert!(!leader_ban_registry::is_on_probation_from_vp(vp), 103); + assert!(vector::length(&ban_registry) == 1, 10); + + // Round 25: Ban expires (duration=8) -> probation + leader_ban_registry::update_ban_registry(0, 25, option::some(1), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::is_on_probation_from_vp(vp), 104); + + // Round 26: Re-ban during probation -> consecutive=2, duration=16 + leader_ban_registry::update_ban_registry(0, 26, option::some(1), vector[0]); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::get_consecutive_count_from_vp(vp) == 2, 11); + assert!(!leader_ban_registry::is_on_probation_from_vp(vp), 105); + assert!(vector::length(&ban_registry) == 1, 12); + + // Round 42: Ban expires (duration=16) -> probation + leader_ban_registry::update_ban_registry(0, 42, option::some(1), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::is_on_probation_from_vp(vp), 106); + + // Round 43: Re-ban during probation -> consecutive=3, duration=min(32,20)=20 + leader_ban_registry::update_ban_registry(0, 43, option::some(1), vector[0]); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::get_consecutive_count_from_vp(vp) == 3, 13); + assert!(!leader_ban_registry::is_on_probation_from_vp(vp), 107); + assert!(vector::length(&ban_registry) == 1, 14); + + // Round 63: Ban expires (duration=20) -> probation + leader_ban_registry::update_ban_registry(0, 63, option::some(1), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::is_on_probation_from_vp(vp), 108); + + // Round 64: Re-ban during probation -> consecutive=4, duration=min(64,20)=20 + leader_ban_registry::update_ban_registry(0, 64, option::some(1), vector[0]); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::get_consecutive_count_from_vp(vp) == 4, 15); + assert!(!leader_ban_registry::is_on_probation_from_vp(vp), 109); + assert!(vector::length(&ban_registry) == 1, 16); + + // ===== Part 3: Natural expiry of max-duration ban ===== + // Ban at round 64 with consecutive=4, duration=20 + // Ban expires at round 84, probation starts at 84, probation expires at 88 + + // At round 83, the ban should still be active (1 round remaining) + leader_ban_registry::update_ban_registry(0, 83, option::some(1), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 1, 17); + let vp = vector::borrow(&ban_registry, 0); + assert!(!leader_ban_registry::is_on_probation_from_vp(vp), 110); // Still banned + + // At round 84, the ban expires and validator transitions to probation + leader_ban_registry::update_ban_registry(0, 84, option::some(1), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 1, 18); // Still in registry (on probation) + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::is_on_probation_from_vp(vp), 111); // Now on probation + + // At round 87, probation should still be active (1 round remaining) + leader_ban_registry::update_ban_registry(0, 87, option::some(1), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 1, 19); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::is_on_probation_from_vp(vp), 112); + + // At round 88, probation expires and validator is fully reinstated + leader_ban_registry::update_ban_registry(0, 88, option::some(1), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 0, 20); // Fully removed from registry + + // ===== Part 4: Multi-validator banning with proposer limit ===== + // Now lets try to fail 3 of them + leader_ban_registry::update_ban_registry(0, 90, option::some(3), vector[0, 1, 2]); + let ban_registry = leader_ban_registry::get_ban_registry(); + // only 2 will be banned because of proposer limit of 2 + assert!(vector::length(&ban_registry) == 2, 21); + + // Re-ban both validators to increase consecutive count + // Round 94: re-ban, consecutive_bans=1, duration=8, resets from round 94 + leader_ban_registry::update_ban_registry( + 0, 90 + (4 * 1), option::some(2), vector[0, 1] + ); + // Round 98: re-ban, consecutive_bans=2, duration=16, resets from round 98 + leader_ban_registry::update_ban_registry( + 0, 90 + (4 * 2), option::some(2), vector[0, 1] + ); + + // ===== Part 5: Cross-epoch ban management ===== + // Now test ban expiry across epoch change WITHOUT re-banning + // Ban was set at round 98 with duration 16 + // First trigger epoch change + leader_ban_registry::on_new_epoch(); + + // In epoch 1, advance rounds without re-banning to let the ban expire naturally + // For validators banned at epoch 0 round 98: + // After on_new_epoch: rounds_served_in_previous_epochs = 0 + // (since epoch_earned == latest_view.epoch and round_earned == latest_view.round) + + // At epoch 1 round 0, ban should still be active (rounds_served = 0 + 0 = 0, need 16) + leader_ban_registry::update_ban_registry(1, 0, option::some(2), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 2, 22); + let vp = vector::borrow(&ban_registry, 0); + assert!(!leader_ban_registry::is_on_probation_from_vp(vp), 113); + + // At epoch 1 round 15, ban should still be active (rounds_served = 0 + 15 = 15, need 16) + leader_ban_registry::update_ban_registry(1, 15, option::some(2), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 2, 23); + + // At epoch 1 round 16, ban expires and validators transition to probation + leader_ban_registry::update_ban_registry(1, 16, option::some(2), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 2, 24); // Still in registry (on probation) + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::is_on_probation_from_vp(vp), 114); + + // At epoch 1 round 19, probation still active (1 round remaining) + leader_ban_registry::update_ban_registry(1, 19, option::some(2), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 2, 25); + + // At epoch 1 round 20, probation expires and validators are fully reinstated + leader_ban_registry::update_ban_registry(1, 20, option::some(2), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 0, 26); + + // ===== Part 6: Re-ban during probation ===== + // Test: Re-banning during probation should increase consecutive count + // Ban validator 1 fresh + leader_ban_registry::update_ban_registry(1, 25, option::some(2), vector[0]); + let ban_registry = leader_ban_registry::get_ban_registry(); + assert!(vector::length(&ban_registry) == 1, 27); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::get_consecutive_count_from_vp(vp) == 0, 115); + assert!(!leader_ban_registry::is_on_probation_from_vp(vp), 116); + + // Ban duration = 4 rounds, so ban expires at round 25 + 4 = 29 + // Transition to probation at round 29 + leader_ban_registry::update_ban_registry(1, 29, option::some(2), vector::empty()); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::is_on_probation_from_vp(vp), 117); + assert!(leader_ban_registry::get_consecutive_count_from_vp(vp) == 0, 118); + + // Re-ban during probation should increase consecutive count and reset to banned state + leader_ban_registry::update_ban_registry(1, 30, option::some(2), vector[0]); + let ban_registry = leader_ban_registry::get_ban_registry(); + let vp = vector::borrow(&ban_registry, 0); + assert!(leader_ban_registry::get_consecutive_count_from_vp(vp) == 1, 119); // Consecutive count increased + assert!(!leader_ban_registry::is_on_probation_from_vp(vp), 120); // Back to banned state + + } +} diff --git a/aptos-move/framework/supra-framework/tests/test_leader_ban_registry_config.move b/aptos-move/framework/supra-framework/tests/test_leader_ban_registry_config.move new file mode 100644 index 0000000000000..80839a1aeb768 --- /dev/null +++ b/aptos-move/framework/supra-framework/tests/test_leader_ban_registry_config.move @@ -0,0 +1,125 @@ +#[test_only] +module supra_framework::test_leader_ban_registry_config { + use std::bcs; + use std::vector; + use supra_framework::config_buffer; + use supra_framework::leader_ban_registry_config; + + #[test(sender = @0xdead)] + #[expected_failure(abort_code = 0x50003, location = supra_framework::system_addresses)] + // signer is not valid + fun test_signer(sender: &signer) { + let ban_registry_param = + leader_ban_registry_config::get_test_ban_registry_params_v0(); + let config_bytes = bcs::to_bytes(&ban_registry_param); + leader_ban_registry_config::initialize(sender, config_bytes); + } + + #[test(sender = @supra_framework)] + #[ + expected_failure( + abort_code = 0x10001, location = supra_framework::leader_ban_registry_config + ) + ] + // Invalid config bytes + fun test_empty_config_value(sender: &signer) { + let config_bytes = vector::empty(); + leader_ban_registry_config::initialize(sender, config_bytes); + } + + #[test(sender = @supra_framework)] + #[ + expected_failure( + abort_code = 0x10003, location = supra_framework::leader_ban_registry_config + ) + ] + // Invalid version bytes + fun test_invalid_config_value(sender: &signer) { + let ban_registry_param = + leader_ban_registry_config::get_test_ban_registry_params_v0(); + let config_bytes = bcs::to_bytes(&ban_registry_param); + vector::push_back(&mut config_bytes, 0); + leader_ban_registry_config::initialize(sender, config_bytes); + } + + public fun init_ban_registry_params(sender: &signer) { + let ban_registry_param = + leader_ban_registry_config::get_test_ban_registry_params_v0(); + let config_bytes = bcs::to_bytes(&ban_registry_param); + leader_ban_registry_config::initialize(sender, config_bytes); + } + + #[test(sender = @supra_framework)] + public fun test_init_config_value(sender: &signer) { + init_ban_registry_params(sender); + assert!(leader_ban_registry_config::check_ban_registry_params_exist(sender), 1); + assert!( + leader_ban_registry_config::check_ban_registry_params_v0_exist(sender), 2 + ); + } + + #[test(sender = @supra_framework)] + #[ + expected_failure( + abort_code = 0x80004, location = supra_framework::leader_ban_registry_config + ) + ] + // Already initialised + fun test_init_config_value_reinit(sender: &signer) { + init_ban_registry_params(sender); + init_ban_registry_params(sender); + } + + #[test(sender = @supra_framework)] + fun test_on_epoch_change(sender: &signer) { + config_buffer::initialize(sender); + assert!( + !leader_ban_registry_config::check_ban_registry_params_exist(sender), 1 + ); + let ban_registry_param = + leader_ban_registry_config::get_test_ban_registry_params_v0(); + let config_bytes = bcs::to_bytes(&ban_registry_param); + leader_ban_registry_config::set_for_next_epoch(sender, config_bytes, 0); + leader_ban_registry_config::on_new_epoch(sender); + assert!(leader_ban_registry_config::check_ban_registry_params_exist(sender), 1); + assert!( + leader_ban_registry_config::check_ban_registry_params_v0_exist(sender), 2 + ); + + let updated_initial_e_denied = 3; + let updated_max_e_denied = 8; + let updated_minimum_u_proposers = 6; + let updated_probation_elections = 2; + + let ban_registry_param = + leader_ban_registry_config::get_custom_ban_registry_params_v0( + updated_initial_e_denied, + updated_max_e_denied, + updated_minimum_u_proposers, + updated_probation_elections + ); + let config_bytes = bcs::to_bytes(&ban_registry_param); + leader_ban_registry_config::set_for_next_epoch(sender, config_bytes, 0); + leader_ban_registry_config::on_new_epoch(sender); + assert!( + leader_ban_registry_config::get_initial_elections_denied() + == updated_initial_e_denied, + 3 + ); + assert!( + leader_ban_registry_config::get_max_elections_denied() + == updated_max_e_denied, + 4 + ); + assert!( + leader_ban_registry_config::get_minimum_unbanned_proposers() + == updated_minimum_u_proposers, + 5 + ); + assert!( + leader_ban_registry_config::get_probation_elections() + == updated_probation_elections, + 6 + ); + } +} diff --git a/aptos-move/framework/supra-stdlib/doc/decode_bcs.md b/aptos-move/framework/supra-stdlib/doc/decode_bcs.md new file mode 100644 index 0000000000000..42393b8d8d485 --- /dev/null +++ b/aptos-move/framework/supra-stdlib/doc/decode_bcs.md @@ -0,0 +1,729 @@ + + + +# Module `0x1::decode_bcs` + +This module implements BCS (de)serialization in Move. +Full specification can be found here: https://github.com/diem/bcs + + +- [Struct `BCS`](#0x1_decode_bcs_BCS) +- [Constants](#@Constants_0) +- [Function `to_bytes`](#0x1_decode_bcs_to_bytes) +- [Function `new`](#0x1_decode_bcs_new) +- [Function `into_remainder_bytes`](#0x1_decode_bcs_into_remainder_bytes) +- [Function `peel_bool`](#0x1_decode_bcs_peel_bool) +- [Function `peel_u8`](#0x1_decode_bcs_peel_u8) +- [Function `peel_u16`](#0x1_decode_bcs_peel_u16) +- [Function `peel_u32`](#0x1_decode_bcs_peel_u32) +- [Function `peel_u64`](#0x1_decode_bcs_peel_u64) +- [Function `peel_u128`](#0x1_decode_bcs_peel_u128) +- [Function `peel_u256`](#0x1_decode_bcs_peel_u256) +- [Function `peel_vec_length`](#0x1_decode_bcs_peel_vec_length) +- [Function `peel_vec_bool`](#0x1_decode_bcs_peel_vec_bool) +- [Function `peel_vec_u8`](#0x1_decode_bcs_peel_vec_u8) +- [Function `peel_vec_u16`](#0x1_decode_bcs_peel_vec_u16) +- [Function `peel_vec_u32`](#0x1_decode_bcs_peel_vec_u32) +- [Function `peel_vec_u64`](#0x1_decode_bcs_peel_vec_u64) +- [Function `peel_vec_u128`](#0x1_decode_bcs_peel_vec_u128) +- [Function `peel_vec_u256`](#0x1_decode_bcs_peel_vec_u256) +- [Function `peel_vec_vec_u8`](#0x1_decode_bcs_peel_vec_vec_u8) +- [Function `peel_vec_vec_vec_u8`](#0x1_decode_bcs_peel_vec_vec_vec_u8) +- [Specification](#@Specification_1) + + +
use 0x1::bcs;
+use 0x1::vector;
+
+ + + + + +## Struct `BCS` + +A helper struct that saves resources on operations. For better +vector performance, it stores reversed bytes of the BCS and +enables use of vector::pop_back. + + +
struct BCS has copy, drop, store
+
+ + + +
+Fields + + +
+
+bytes: vector<u8> +
+
+ +
+
+ + +
+ + + +## Constants + + + + +For when ULEB byte is out of range (or not found). + + +
const ELenOutOfRange: u64 = 2;
+
+ + + + + +For when the boolean value different than 0 or 1. + + +
const ENotBool: u64 = 1;
+
+ + + + + +For when bytes length is less than required for deserialization. + + +
const EOutOfRange: u64 = 0;
+
+ + + + + +## Function `to_bytes` + +Get BCS serialized bytes for any value. +Re-exports stdlib bcs::to_bytes. + + +
public fun to_bytes<T>(value: &T): vector<u8>
+
+ + + +
+Implementation + + +
public fun to_bytes<T>(value: &T): vector<u8> {
+    bcs::to_bytes(value)
+}
+
+ + + +
+ + + +## Function `new` + +Creates a new instance of BCS wrapper that holds inversed +bytes for better performance. + + +
public fun new(bytes: vector<u8>): decode_bcs::BCS
+
+ + + +
+Implementation + + +
public fun new(bytes: vector<u8>): BCS {
+    v::reverse(&mut bytes);
+    BCS { bytes }
+}
+
+ + + +
+ + + +## Function `into_remainder_bytes` + +Unpack the BCS struct returning the leftover bytes. +Useful for passing the data further after partial deserialization. + + +
public fun into_remainder_bytes(bcs: decode_bcs::BCS): vector<u8>
+
+ + + +
+Implementation + + +
public fun into_remainder_bytes(bcs: BCS): vector<u8> {
+    let BCS { bytes } = bcs;
+    v::reverse(&mut bytes);
+    bytes
+}
+
+ + + +
+ + + +## Function `peel_bool` + +Read a bool value from bcs-serialized bytes. + + +
public fun peel_bool(bcs: &mut decode_bcs::BCS): bool
+
+ + + +
+Implementation + + +
public fun peel_bool(bcs: &mut BCS): bool {
+    let value = peel_u8(bcs);
+    if (value == 0) {
+        false
+    } else if (value == 1) {
+        true
+    } else {
+        abort ENotBool
+    }
+}
+
+ + + +
+ + + +## Function `peel_u8` + +Read u8 value from bcs-serialized bytes. + + +
public fun peel_u8(bcs: &mut decode_bcs::BCS): u8
+
+ + + +
+Implementation + + +
public fun peel_u8(bcs: &mut BCS): u8 {
+    assert!(v::length(&bcs.bytes) >= 1, EOutOfRange);
+    v::pop_back(&mut bcs.bytes)
+}
+
+ + + +
+ + + +## Function `peel_u16` + +Read u16 value from bcs-serialized bytes. + + +
public fun peel_u16(bcs: &mut decode_bcs::BCS): u16
+
+ + + +
+Implementation + + +
public fun peel_u16(bcs: &mut BCS): u16 {
+    assert!(v::length(&bcs.bytes) >= 2, EOutOfRange);
+    let (value, i) = (0u16, 0u8);
+    while (i < 16) {
+        let byte = (v::pop_back(&mut bcs.bytes) as u16);
+        value = value | (byte << i);
+        i = i + 8;
+    };
+    value
+}
+
+ + + +
+ + + +## Function `peel_u32` + +Read u32 value from bcs-serialized bytes. + + +
public fun peel_u32(bcs: &mut decode_bcs::BCS): u32
+
+ + + +
+Implementation + + +
public fun peel_u32(bcs: &mut BCS): u32 {
+    assert!(v::length(&bcs.bytes) >= 4, EOutOfRange);
+    let (value, i) = (0u32, 0u8);
+    while (i < 32) {
+        let byte = (v::pop_back(&mut bcs.bytes) as u32);
+        value = value | (byte << i);
+        i = i + 8;
+    };
+    value
+}
+
+ + + +
+ + + +## Function `peel_u64` + +Read u64 value from bcs-serialized bytes. + + +
public fun peel_u64(bcs: &mut decode_bcs::BCS): u64
+
+ + + +
+Implementation + + +
public fun peel_u64(bcs: &mut BCS): u64 {
+    assert!(v::length(&bcs.bytes) >= 8, EOutOfRange);
+    let (value, i) = (0u64, 0u8);
+    while (i < 64) {
+        let byte = (v::pop_back(&mut bcs.bytes) as u64);
+        value = value | (byte << i);
+        i = i + 8;
+    };
+    value
+}
+
+ + + +
+ + + +## Function `peel_u128` + +Read u128 value from bcs-serialized bytes. + + +
public fun peel_u128(bcs: &mut decode_bcs::BCS): u128
+
+ + + +
+Implementation + + +
public fun peel_u128(bcs: &mut BCS): u128 {
+    assert!(v::length(&bcs.bytes) >= 16, EOutOfRange);
+
+    let (value, i) = (0u128, 0u8);
+    while (i < 128) {
+        let byte = (v::pop_back(&mut bcs.bytes) as u128);
+        value = value | (byte << i);
+        i = i + 8;
+    };
+
+    value
+}
+
+ + + +
+ + + +## Function `peel_u256` + +Read u256 value from bcs-serialized bytes. + + +
public fun peel_u256(bcs: &mut decode_bcs::BCS): u256
+
+ + + +
+Implementation + + +
public fun peel_u256(bcs: &mut BCS): u256 {
+    assert!(v::length(&bcs.bytes) >= 16, EOutOfRange);
+
+    let (value, i) = (0u256, 0u8);
+    while (i < 255) {
+        let byte = (v::pop_back(&mut bcs.bytes) as u256);
+        value = value | (byte << i);
+        i = i + 8;
+    };
+
+    value
+}
+
+ + + +
+ + + +## Function `peel_vec_length` + +Read ULEB bytes expecting a vector length. Result should +then be used to perform peel_* operation LEN times. + +In BCS vector length is implemented with ULEB128; +See more here: https://en.wikipedia.org/wiki/LEB128 + + +
public fun peel_vec_length(bcs: &mut decode_bcs::BCS): u64
+
+ + + +
+Implementation + + +
public fun peel_vec_length(bcs: &mut BCS): u64 {
+    let (total, shift, len) = (0u64, 0, 0);
+    while (true) {
+        assert!(len <= 4, ELenOutOfRange);
+        let byte = (v::pop_back(&mut bcs.bytes) as u64);
+        len = len + 1;
+        total = total | ((byte & 0x7f) << shift);
+        if ((byte & 0x80) == 0) {
+            break
+        };
+        shift = shift + 7;
+    };
+    total
+}
+
+ + + +
+ + + +## Function `peel_vec_bool` + +Peel a vector of bool from serialized bytes. + + +
public fun peel_vec_bool(bcs: &mut decode_bcs::BCS): vector<bool>
+
+ + + +
+Implementation + + +
public fun peel_vec_bool(bcs: &mut BCS): vector<bool> {
+    let (len, i, res) = (peel_vec_length(bcs), 0, vector[]);
+    while (i < len) {
+        v::push_back(&mut res, peel_bool(bcs));
+        i = i + 1;
+    };
+    res
+}
+
+ + + +
+ + + +## Function `peel_vec_u8` + +Peel a vector of u8 (eg string) from serialized bytes. + + +
public fun peel_vec_u8(bcs: &mut decode_bcs::BCS): vector<u8>
+
+ + + +
+Implementation + + +
public fun peel_vec_u8(bcs: &mut BCS): vector<u8> {
+    let (len, i, res) = (peel_vec_length(bcs), 0, vector[]);
+    while (i < len) {
+        v::push_back(&mut res, peel_u8(bcs));
+        i = i + 1;
+    };
+    res
+}
+
+ + + +
+ + + +## Function `peel_vec_u16` + +Peel a vector of u16 (eg string) from serialized bytes. + + +
public fun peel_vec_u16(bcs: &mut decode_bcs::BCS): vector<u16>
+
+ + + +
+Implementation + + +
public fun peel_vec_u16(bcs: &mut BCS): vector<u16> {
+    let (len, i, res) = (peel_vec_length(bcs), 0, vector[]);
+    while (i < len) {
+        v::push_back(&mut res, peel_u16(bcs));
+        i = i + 1;
+    };
+    res
+}
+
+ + + +
+ + + +## Function `peel_vec_u32` + +Peel a vector of u32 (eg string) from serialized bytes. + + +
public fun peel_vec_u32(bcs: &mut decode_bcs::BCS): vector<u32>
+
+ + + +
+Implementation + + +
public fun peel_vec_u32(bcs: &mut BCS): vector<u32> {
+    let (len, i, res) = (peel_vec_length(bcs), 0, vector[]);
+    while (i < len) {
+        v::push_back(&mut res, peel_u32(bcs));
+        i = i + 1;
+    };
+    res
+}
+
+ + + +
+ + + +## Function `peel_vec_u64` + +Peel a vector of u64 from serialized bytes. + + +
public fun peel_vec_u64(bcs: &mut decode_bcs::BCS): vector<u64>
+
+ + + +
+Implementation + + +
public fun peel_vec_u64(bcs: &mut BCS): vector<u64> {
+    let (len, i, res) = (peel_vec_length(bcs), 0, vector[]);
+    while (i < len) {
+        v::push_back(&mut res, peel_u64(bcs));
+        i = i + 1;
+    };
+    res
+}
+
+ + + +
+ + + +## Function `peel_vec_u128` + +Peel a vector of u128 from serialized bytes. + + +
public fun peel_vec_u128(bcs: &mut decode_bcs::BCS): vector<u128>
+
+ + + +
+Implementation + + +
public fun peel_vec_u128(bcs: &mut BCS): vector<u128> {
+    let (len, i, res) = (peel_vec_length(bcs), 0, vector[]);
+    while (i < len) {
+        v::push_back(&mut res, peel_u128(bcs));
+        i = i + 1;
+    };
+    res
+}
+
+ + + +
+ + + +## Function `peel_vec_u256` + +Peel a vector of u256 from serialized bytes. + + +
public fun peel_vec_u256(bcs: &mut decode_bcs::BCS): vector<u256>
+
+ + + +
+Implementation + + +
public fun peel_vec_u256(bcs: &mut BCS): vector<u256> {
+    let (len, i, res) = (peel_vec_length(bcs), 0, vector[]);
+    while (i < len) {
+        v::push_back(&mut res, peel_u256(bcs));
+        i = i + 1;
+    };
+    res
+}
+
+ + + +
+ + + +## Function `peel_vec_vec_u8` + +Peel a vector<vector<u8>> (eg vec of string) from serialized bytes. + + +
public fun peel_vec_vec_u8(bcs: &mut decode_bcs::BCS): vector<vector<u8>>
+
+ + + +
+Implementation + + +
public fun peel_vec_vec_u8(bcs: &mut BCS): vector<vector<u8>> {
+    let (len, i, res) = (peel_vec_length(bcs), 0, vector[]);
+    while (i < len) {
+        v::push_back(&mut res, peel_vec_u8(bcs));
+        i = i + 1;
+    };
+    res
+}
+
+ + + +
+ + + +## Function `peel_vec_vec_vec_u8` + +Peel a vector<vector<vector<u8>>> (eg vec of string) from serialized bytes. + + +
public fun peel_vec_vec_vec_u8(bcs: &mut decode_bcs::BCS): vector<vector<vector<u8>>>
+
+ + + +
+Implementation + + +
public fun peel_vec_vec_vec_u8(bcs: &mut BCS): vector<vector<vector<u8>>> {
+    let (len, i, res) = (peel_vec_length(bcs), 0, vector[]);
+    while (i < len) {
+        v::push_back(&mut res, peel_vec_vec_u8(bcs));
+        i = i + 1;
+    };
+    res
+}
+
+ + + +
+ + + +## Specification + + + +
pragma verify = false;
+
+ + +[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/supra-stdlib/doc/overview.md b/aptos-move/framework/supra-stdlib/doc/overview.md index 08a5aa31ff618..e880ae5616a31 100644 --- a/aptos-move/framework/supra-stdlib/doc/overview.md +++ b/aptos-move/framework/supra-stdlib/doc/overview.md @@ -15,6 +15,7 @@ This is the reference documentation of the Supra standard library extension. - [`0x1::bls12381_bulletproofs`](bls12381_bulletproofs.md#0x1_bls12381_bulletproofs) - [`0x1::bls12381_pedersen`](bls12381_pedersen.md#0x1_bls12381_pedersen) - [`0x1::bls12381_scalar`](bls12381_scalar.md#0x1_bls12381_scalar) +- [`0x1::decode_bcs`](decode_bcs.md#0x1_decode_bcs) - [`0x1::enumerable_map`](enumerable_map.md#0x1_enumerable_map) - [`0x1::eth_trie`](eth_trie.md#0x1_eth_trie) - [`0x1::rlp`](rlp.md#0x1_rlp) diff --git a/aptos-move/framework/supra-stdlib/sources/decode_bcs.move b/aptos-move/framework/supra-stdlib/sources/decode_bcs.move new file mode 100644 index 0000000000000..5bf4aaca7dfdf --- /dev/null +++ b/aptos-move/framework/supra-stdlib/sources/decode_bcs.move @@ -0,0 +1,370 @@ +/// This module implements BCS (de)serialization in Move. +/// Full specification can be found here: https://github.com/diem/bcs +module supra_std::decode_bcs { + use std::vector as v; + use std::bcs; + + /// For when bytes length is less than required for deserialization. + const EOutOfRange: u64 = 0; + + /// For when the boolean value different than `0` or `1`. + const ENotBool: u64 = 1; + + /// For when ULEB byte is out of range (or not found). + const ELenOutOfRange: u64 = 2; + + /// A helper struct that saves resources on operations. For better + /// vector performance, it stores reversed bytes of the BCS and + /// enables use of `vector::pop_back`. + struct BCS has store, copy, drop { + bytes: vector + } + + /// Get BCS serialized bytes for any value. + /// Re-exports stdlib `bcs::to_bytes`. + public fun to_bytes(value: &T): vector { + bcs::to_bytes(value) + } + + /// Creates a new instance of BCS wrapper that holds inversed + /// bytes for better performance. + public fun new(bytes: vector): BCS { + v::reverse(&mut bytes); + BCS { bytes } + } + + /// Unpack the `BCS` struct returning the leftover bytes. + /// Useful for passing the data further after partial deserialization. + public fun into_remainder_bytes(bcs: BCS): vector { + let BCS { bytes } = bcs; + v::reverse(&mut bytes); + bytes + } + + /// Read a `bool` value from bcs-serialized bytes. + public fun peel_bool(bcs: &mut BCS): bool { + let value = peel_u8(bcs); + if (value == 0) { + false + } else if (value == 1) { + true + } else { + abort ENotBool + } + } + + /// Read `u8` value from bcs-serialized bytes. + public fun peel_u8(bcs: &mut BCS): u8 { + assert!(v::length(&bcs.bytes) >= 1, EOutOfRange); + v::pop_back(&mut bcs.bytes) + } + + /// Read `u16` value from bcs-serialized bytes. + public fun peel_u16(bcs: &mut BCS): u16 { + assert!(v::length(&bcs.bytes) >= 2, EOutOfRange); + let (value, i) = (0u16, 0u8); + while (i < 16) { + let byte = (v::pop_back(&mut bcs.bytes) as u16); + value = value | (byte << i); + i = i + 8; + }; + value + } + + /// Read `u32` value from bcs-serialized bytes. + public fun peel_u32(bcs: &mut BCS): u32 { + assert!(v::length(&bcs.bytes) >= 4, EOutOfRange); + let (value, i) = (0u32, 0u8); + while (i < 32) { + let byte = (v::pop_back(&mut bcs.bytes) as u32); + value = value | (byte << i); + i = i + 8; + }; + value + } + + /// Read `u64` value from bcs-serialized bytes. + public fun peel_u64(bcs: &mut BCS): u64 { + assert!(v::length(&bcs.bytes) >= 8, EOutOfRange); + let (value, i) = (0u64, 0u8); + while (i < 64) { + let byte = (v::pop_back(&mut bcs.bytes) as u64); + value = value | (byte << i); + i = i + 8; + }; + value + } + + /// Read `u128` value from bcs-serialized bytes. + public fun peel_u128(bcs: &mut BCS): u128 { + assert!(v::length(&bcs.bytes) >= 16, EOutOfRange); + + let (value, i) = (0u128, 0u8); + while (i < 128) { + let byte = (v::pop_back(&mut bcs.bytes) as u128); + value = value | (byte << i); + i = i + 8; + }; + + value + } + + /// Read `u256` value from bcs-serialized bytes. + public fun peel_u256(bcs: &mut BCS): u256 { + assert!(v::length(&bcs.bytes) >= 16, EOutOfRange); + + let (value, i) = (0u256, 0u8); + while (i < 255) { + let byte = (v::pop_back(&mut bcs.bytes) as u256); + value = value | (byte << i); + i = i + 8; + }; + + value + } + + // === Vector === + + /// Read ULEB bytes expecting a vector length. Result should + /// then be used to perform `peel_*` operation LEN times. + /// + /// In BCS `vector` length is implemented with ULEB128; + /// See more here: https://en.wikipedia.org/wiki/LEB128 + public fun peel_vec_length(bcs: &mut BCS): u64 { + let (total, shift, len) = (0u64, 0, 0); + while (true) { + assert!(len <= 4, ELenOutOfRange); + let byte = (v::pop_back(&mut bcs.bytes) as u64); + len = len + 1; + total = total | ((byte & 0x7f) << shift); + if ((byte & 0x80) == 0) { + break + }; + shift = shift + 7; + }; + total + } + + /// Peel a vector of `bool` from serialized bytes. + public fun peel_vec_bool(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_bool(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `u8` (eg string) from serialized bytes. + public fun peel_vec_u8(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_u8(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `u16` (eg string) from serialized bytes. + public fun peel_vec_u16(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_u16(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `u32` (eg string) from serialized bytes. + public fun peel_vec_u32(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_u32(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `u64` from serialized bytes. + public fun peel_vec_u64(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_u64(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `u128` from serialized bytes. + public fun peel_vec_u128(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_u128(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `u256` from serialized bytes. + public fun peel_vec_u256(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_u256(bcs)); + i = i + 1; + }; + res + } + + /// Peel a `vector>` (eg vec of string) from serialized bytes. + public fun peel_vec_vec_u8(bcs: &mut BCS): vector> { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_vec_u8(bcs)); + i = i + 1; + }; + res + } + + /// Peel a `vector>>` (eg vec of string) from serialized bytes. + public fun peel_vec_vec_vec_u8(bcs: &mut BCS): vector>> { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_vec_vec_u8(bcs)); + i = i + 1; + }; + res + } + // TODO: re-enable once bit-wise operators in peel_vec_length are supported in the prover + spec module { pragma verify = false; } + + // === Tests === + + #[test_only] + struct Info has drop { a: bool, b: u8, c: u64, d: u128, k: vector, s: address } + + #[test] + #[expected_failure(abort_code = ELenOutOfRange)] + fun test_uleb_len_fail() { + let value = vector[0xff, 0xff, 0xff, 0xff, 0xff]; + let bytes = new(to_bytes(&value)); + let _fail = peel_vec_length(&mut bytes); + abort 2 // TODO: make this test fail + } + + #[test] + #[expected_failure(abort_code = ENotBool)] + fun test_bool_fail() { + let bytes = new(to_bytes(&10u8)); + let _fail = peel_bool(&mut bytes); + } + + #[test] + fun test_bcs() { + + { // boolean: true + let value = true; + let bytes = new(to_bytes(&value)); + assert!(value == peel_bool(&mut bytes), 0); + }; + + { // boolean: false + let value = false; + let bytes = new(to_bytes(&value)); + assert!(value == peel_bool(&mut bytes), 0); + }; + + { // u8 + let value = 100u8; + let bytes = new(to_bytes(&value)); + assert!(value == peel_u8(&mut bytes), 0); + }; + + { // u64 (4 bytes) + let value = 1000100u64; + let bytes = new(to_bytes(&value)); + assert!(value == peel_u64(&mut bytes), 0); + }; + + { // u64 (8 bytes) + let value = 100000000000000u64; + let bytes = new(to_bytes(&value)); + assert!(value == peel_u64(&mut bytes), 0); + }; + + { // u128 (16 bytes) + let value = 100000000000000000000000000u128; + let bytes = new(to_bytes(&value)); + assert!(value == peel_u128(&mut bytes), 0); + }; + + { // vector length + let value = vector[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let bytes = new(to_bytes(&value)); + assert!(v::length(&value) == peel_vec_length(&mut bytes), 0); + }; + + { // vector length (more data) + let value = vector[ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ]; + + let bytes = new(to_bytes(&value)); + assert!(v::length(&value) == peel_vec_length(&mut bytes), 0); + }; + + { // full deserialization test (ordering) + let info = Info { a: true, b: 100, c: 9999, d: 112333, k: vector[true, false, true, false], s: @0xAAAAAAAAAAA }; + let bytes = new(to_bytes(&info)); + + assert!(info.a == peel_bool(&mut bytes), 0); + assert!(info.b == peel_u8(&mut bytes), 0); + assert!(info.c == peel_u64(&mut bytes), 0); + assert!(info.d == peel_u128(&mut bytes), 0); + + let len = peel_vec_length(&mut bytes); + + assert!(v::length(&info.k) == len, 0); + + }; + + { // read vector of bytes directly + let value = vector[ + vector[1,2,3,4,5], + vector[1,2,3,4,5], + vector[1,2,3,4,5] + ]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_vec_u8(&mut bytes), 0); + }; + + { // read vector of bytes directly + let value = vector[1,2,3,4,5]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_u8(&mut bytes), 0); + }; + + { // read vector of bytes directly + let value = vector[1,2,3,4,5]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_u64(&mut bytes), 0); + }; + + { // read vector of bytes directly + let value = vector[1,2,3,4,5]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_u128(&mut bytes), 0); + }; + + { // read vector of bytes directly + let value = vector[true, false, true, false]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_bool(&mut bytes), 0); + }; + + } +} \ No newline at end of file