diff --git a/.gitignore b/.gitignore index 21446d574..1a8778d90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Generated by Cargo # will have compiled files and executables -/target/ +**/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html @@ -24,4 +24,8 @@ sccache.log *.bpl # exclude diem dependency -diem/ \ No newline at end of file +diem/ + +# exclude move coverage +.coverage_map.mvcov +.trace \ No newline at end of file diff --git a/framework/cached-packages/src/libra_framework_sdk_builder.rs b/framework/cached-packages/src/libra_framework_sdk_builder.rs index 4458a648e..83266963c 100644 --- a/framework/cached-packages/src/libra_framework_sdk_builder.rs +++ b/framework/cached-packages/src/libra_framework_sdk_builder.rs @@ -32,6 +32,8 @@ type Bytes = Vec; /// } /// ``` #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "fuzzing", derive(proptest_derive::Arbitrary))] +#[cfg_attr(feature = "fuzzing", proptest(no_params))] pub enum EntryFunctionCall { /// Offers rotation capability on behalf of `account` to the account at address `recipient_address`. /// An account can delegate its rotation capability to only one other address at one time. If the account @@ -93,14 +95,14 @@ pub enum EntryFunctionCall { /// Generic authentication key rotation function that allows the user to rotate their authentication key from any scheme to any scheme. /// To authorize the rotation, we need two signatures: /// - the first signature `cap_rotate_key` refers to the signature by the account owner's current key on a valid `RotationProofChallenge`, - /// demonstrating that the user intends to and has the capability to rotate the authentication key of this account; + /// demonstrating that the user intends to and has the capability to rotate the authentication key of this account; /// - the second signature `cap_update_table` refers to the signature by the new key (that the account owner wants to rotate to) on a - /// valid `RotationProofChallenge`, demonstrating that the user owns the new private key, and has the authority to update the - /// `OriginatingAddress` map with the new address mapping ``. - /// To verify these two signatures, we need their corresponding public key and public key scheme: we use `from_scheme` and `from_public_key_bytes` - /// to verify `cap_rotate_key`, and `to_scheme` and `to_public_key_bytes` to verify `cap_update_table`. - /// A scheme of 0 refers to an Ed25519 key and a scheme of 1 refers to Multi-Ed25519 keys. - /// `originating address` refers to an account's original/first address. + /// valid `RotationProofChallenge`, demonstrating that the user owns the new private key, and has the authority to update the + /// `OriginatingAddress` map with the new address mapping ``. + /// To verify these two signatures, we need their corresponding public key and public key scheme: we use `from_scheme` and `from_public_key_bytes` + /// to verify `cap_rotate_key`, and `to_scheme` and `to_public_key_bytes` to verify `cap_update_table`. + /// A scheme of 0 refers to an Ed25519 key and a scheme of 1 refers to Multi-Ed25519 keys. + /// `originating address` refers to an account's original/first address. /// /// Here is an example attack if we don't ask for the second signature `cap_update_table`: /// Alice has rotated her account `addr_a` to `new_addr_a`. As a result, the following entry is created, to help Alice when recovering her wallet: @@ -1027,14 +1029,14 @@ pub fn account_revoke_signer_capability( /// Generic authentication key rotation function that allows the user to rotate their authentication key from any scheme to any scheme. /// To authorize the rotation, we need two signatures: /// - the first signature `cap_rotate_key` refers to the signature by the account owner's current key on a valid `RotationProofChallenge`, -/// demonstrating that the user intends to and has the capability to rotate the authentication key of this account; +/// demonstrating that the user intends to and has the capability to rotate the authentication key of this account; /// - the second signature `cap_update_table` refers to the signature by the new key (that the account owner wants to rotate to) on a -/// valid `RotationProofChallenge`, demonstrating that the user owns the new private key, and has the authority to update the -/// `OriginatingAddress` map with the new address mapping ``. -/// To verify these two signatures, we need their corresponding public key and public key scheme: we use `from_scheme` and `from_public_key_bytes` -/// to verify `cap_rotate_key`, and `to_scheme` and `to_public_key_bytes` to verify `cap_update_table`. -/// A scheme of 0 refers to an Ed25519 key and a scheme of 1 refers to Multi-Ed25519 keys. -/// `originating address` refers to an account's original/first address. +/// valid `RotationProofChallenge`, demonstrating that the user owns the new private key, and has the authority to update the +/// `OriginatingAddress` map with the new address mapping ``. +/// To verify these two signatures, we need their corresponding public key and public key scheme: we use `from_scheme` and `from_public_key_bytes` +/// to verify `cap_rotate_key`, and `to_scheme` and `to_public_key_bytes` to verify `cap_update_table`. +/// A scheme of 0 refers to an Ed25519 key and a scheme of 1 refers to Multi-Ed25519 keys. +/// `originating address` refers to an account's original/first address. /// /// Here is an example attack if we don't ask for the second signature `cap_update_table`: /// Alice has rotated her account `addr_a` to `new_addr_a`. As a result, the following entry is created, to help Alice when recovering her wallet: diff --git a/framework/drop-user-tools/jail_with_gc.move b/framework/drop-user-tools/jail_with_gc.move index cab301d38..1e355fcd0 100644 --- a/framework/drop-user-tools/jail_with_gc.move +++ b/framework/drop-user-tools/jail_with_gc.move @@ -160,31 +160,21 @@ module ol_framework::jail { }; } - /// gets a list of validators based on their jail reputation + /// Sort validators based on their jail reputation /// this is used in the bidding process for Proof-of-Fee where /// we seat the validators with the least amount of consecutive failures /// to rejoin. public(friend) fun sort_by_jail(vec_address: vector
): vector
acquires Jail { - - // Sorting the accounts vector based on value (weights). - // Bubble sort algorithm - let length = vector::length(&vec_address); - - let i = 0; - while (i < length){ - let j = 0; - while(j < length-i-1){ - - let (_, value_j) = get_jail_reputation(*vector::borrow(&vec_address, j)); - let (_, value_jp1) = get_jail_reputation(*vector::borrow(&vec_address, j + 1)); - - if(value_j > value_jp1){ - vector::swap
(&mut vec_address, j, j+1); - }; - j = j + 1; - }; - i = i + 1; - }; + // get the reputation of the validators + let reputation = vector::map(vec_address, |addr| { + let (_, value) = get_jail_reputation(addr); + value + }); + + // sort the validators based on their reputation + address_utils::sort_by_values(&mut vec_address, &mut reputation); + // shuffle duplicates scores to ensure fairness + address_utils::shuffle_duplicates(&mut vec_address, &reputation); vec_address } diff --git a/framework/libra-framework/sources/genesis.move b/framework/libra-framework/sources/genesis.move index 161e92c1b..f61594a56 100644 --- a/framework/libra-framework/sources/genesis.move +++ b/framework/libra-framework/sources/genesis.move @@ -24,6 +24,7 @@ module diem_framework::genesis { use diem_framework::transaction_fee; use diem_framework::transaction_validation; use diem_framework::version; + use diem_framework::randomness; //////// 0L //////// use diem_framework::validator_universe; @@ -137,6 +138,7 @@ module diem_framework::genesis { reconfiguration::initialize(&diem_framework_account); block::initialize(&diem_framework_account, epoch_interval_microsecs); state_storage::initialize(&diem_framework_account); + randomness::initialize(&diem_framework_account); //////// 0L //////// diff --git a/framework/libra-framework/sources/ol_sources/address_utils.move b/framework/libra-framework/sources/ol_sources/address_utils.move new file mode 100644 index 000000000..dd696932a --- /dev/null +++ b/framework/libra-framework/sources/ol_sources/address_utils.move @@ -0,0 +1,229 @@ +/// This module provides utility functions for handling and manipulating vectors of +/// addresses and their corresponding values. + +module ol_framework::address_utils { + use std::error; + use std::vector; + use diem_framework::randomness; + + // Error code for different length of addresses and values + const EDIFFERENT_LENGTH: u64 = 1; + + // A, B, C, easy as 1, 2, 3 + + // Bubble sort addresses and corresponding values + public fun sort_by_values(addresses: &mut vector
, values: &mut vector) { + assert!(vector::length(addresses) == vector::length(values), error::invalid_argument(EDIFFERENT_LENGTH)); + let len = vector::length(values); + let i = 0; + while (i < len) { + let j = 0; + while (j < len - i - 1) { + let value_j = *vector::borrow(values, j); + let value_jp1 = *vector::borrow(values, j + 1); + if (value_j > value_jp1) { + vector::swap(values, j, j + 1); + vector::swap
(addresses, j, j + 1); + }; + j = j + 1; + }; + i = i + 1; + }; + } + + // Shuffle addresses with the same values to ensure randomness position + public fun shuffle_duplicates(addresses: &mut vector
, values: &vector) { + assert!(vector::length(addresses) == vector::length(values), error::invalid_argument(EDIFFERENT_LENGTH)); + let len = vector::length(values); + let i = 0; + while (i < len) { + let j = i + 1; + while (j < len && *vector::borrow(values, i) == *vector::borrow(values, j)) { + j = j + 1; + }; + if (j > i + 1) { + let slice_len = j - i; + let perm = randomness::permutation(slice_len); + let temp_addresses = vector::empty
(); + let k = 0; + while (k < slice_len) { + let pos = i + k; + vector::push_back(&mut temp_addresses, *vector::borrow(addresses, pos)); + k = k + 1; + }; + k = 0; + while (k < slice_len) { + let perm_pos = *vector::borrow(&perm, k); + *vector::borrow_mut(addresses, i + k) = *vector::borrow(&temp_addresses, perm_pos); + k = k + 1; + }; + }; + i = j; + }; + } + + // Bubble Sort tests + + #[test] + #[expected_failure(abort_code = 0x10001, location = ol_framework::address_utils)] + fun test_sort_by_values_different_lengths() { + let values = vector[1, 2, 3]; + let addresses = vector[@0x1, @0x2]; + // This should trigger an assertion error due to different lengths + sort_by_values(&mut addresses, &mut values); + } + + #[test] + fun test_sort_empty_vectors() { + let values: vector = vector::empty(); + let addresses: vector
= vector::empty(); + sort_by_values(&mut addresses, &mut values); + assert!(values == vector[], 10002); + assert!(addresses == vector[], 10003); + } + + #[test] + fun test_sort_single_element() { + let values = vector[10]; + let addresses = vector[@0x1]; + sort_by_values(&mut addresses, &mut values); + assert!(values == vector[10], 10004); + assert!(addresses == vector[@0x1], 10005); + } + + #[test] + fun test_sort_already_sorted() { + let values = vector[1, 2, 3, 4, 5]; + let addresses = vector[@0x1, @0x2, @0x3, @0x4, @0x5]; + sort_by_values(&mut addresses, &mut values); + assert!(values == vector[1, 2, 3, 4, 5], 10006); + assert!(addresses == vector[@0x1, @0x2, @0x3, @0x4, @0x5], 10007); + } + + #[test] + fun test_sort_reverse_order() { + let values = vector[5, 4, 3, 2, 1]; + let addresses = vector[@0x5, @0x4, @0x3, @0x2, @0x1]; + sort_by_values(&mut addresses, &mut values); + assert!(values == vector[1, 2, 3, 4, 5], 10008); + assert!(addresses == vector[@0x1, @0x2, @0x3, @0x4, @0x5], 10009); + } + + #[test] + fun test_sort_with_duplicates() { + let values = vector[4, 2, 2, 3, 1]; + let addresses = vector[@0x1, @0x2, @0x3, @0x4, @0x5]; + sort_by_values(&mut addresses, &mut values); + assert!(values == vector[1, 2, 2, 3, 4], 10010); + assert!(addresses == vector[@0x5, @0x2, @0x3, @0x4, @0x1], 10011); + } + + #[test] + fun test_sort_random_order() { + let values = vector[3, 1, 4, 5, 2]; + let addresses = vector[@0x1, @0x2, @0x3, @0x4, @0x5]; + sort_by_values(&mut addresses, &mut values); + assert!(values == vector[1, 2, 3, 4, 5], 10012); + assert!(addresses == vector[@0x2, @0x5, @0x1, @0x3, @0x4], 10013); + } + + #[test] + fun test_sort_all_elements_equal() { + let values = vector[3, 3, 3, 3, 3]; + let addresses = vector[@0x1, @0x2, @0x3, @0x4, @0x5]; + sort_by_values(&mut addresses, &mut values); + assert!(values == vector[3, 3, 3, 3, 3], 10014); + assert!(addresses == vector[@0x1, @0x2, @0x3, @0x4, @0x5], 10015); + } + + + // Shuffle Tests + + #[test] + #[expected_failure(abort_code = 0x10001, location = ol_framework::address_utils)] + fun test_shuffle_duplicates_different_lengths() { + let values = vector[1, 2, 3]; + let addresses = vector[@0x1, @0x2]; + // This should trigger an assertion error due to different lengths + shuffle_duplicates(&mut addresses, &mut values); + } + + #[test] + fun test_shuffle_no_duplicates() { + // No duplicates in the values vector + let values = vector[1, 2, 3, 4, 5]; + let addresses = vector[@0x1, @0x2, @0x3, @0x4, @0x5]; + shuffle_duplicates(&mut addresses, &mut values); + assert!(values == vector[1, 2, 3, 4, 5], 10017); + assert!(addresses == vector[@0x1, @0x2, @0x3, @0x4, @0x5], 10018); + } + + #[test(root = @ol_framework)] + fun test_shuffle_with_duplicates(root: &signer) { + // One group of duplicates in the values vector + randomness::initialize_for_testing(root); + let values = vector[1, 2, 2, 3, 4]; + let addresses = vector[@0x1, @0x2, @0x3, @0x4, @0x5]; + let original_addresses = vector[@0x1, @0x2, @0x3, @0x4, @0x5]; + let shuffled = false; + let i = 0; + + while (i < 10) { + shuffle_duplicates(&mut addresses, &mut values); + if (addresses != original_addresses) { + shuffled = true; + break + }; + i = i + 1; + }; + + assert!(values == vector[1, 2, 2, 3, 4], 10019); + assert!(shuffled, 10020); + } + + #[test(root = @ol_framework)] + fun test_shuffle_multiple_duplicate_groups(root: &signer) { + // Multiple groups of duplicates in the values vector + randomness::initialize_for_testing(root); + let values = vector[1, 2, 2, 3, 3, 4]; + let addresses = vector[@0x1, @0x2, @0x3, @0x4, @0x5, @0x6]; + let original_addresses = vector[@0x1, @0x2, @0x3, @0x4, @0x5, @0x6]; + let shuffled = false; + let i = 0; + + while (i < 10) { + shuffle_duplicates(&mut addresses, &mut values); + if (addresses != original_addresses) { + shuffled = true; + break + }; + i = i + 1; + }; + + assert!(values == vector[1, 2, 2, 3, 3, 4], 10021); + assert!(shuffled, 10022); + } + + #[test(root = @ol_framework)] + fun test_shuffle_all_elements_equal(root: &signer) { + // All elements in the values vector are the same + randomness::initialize_for_testing(root); + let values = vector[2, 2, 2, 2]; + let addresses = vector[@0x1, @0x2, @0x3, @0x4]; + let original_addresses = vector[@0x1, @0x2, @0x3, @0x4]; + let shuffled = false; + let i = 0; + + while (i < 10) { + shuffle_duplicates(&mut addresses, &mut values); + if (addresses != original_addresses) { + shuffled = true; + break + }; + i = i + 1; + }; + + assert!(values == vector[2, 2, 2, 2], 10023); + assert!(shuffled, 10024); + } +} diff --git a/framework/libra-framework/sources/ol_sources/ancestry.move b/framework/libra-framework/sources/ol_sources/ancestry.move index 3bed49f99..0bb89368c 100644 --- a/framework/libra-framework/sources/ol_sources/ancestry.move +++ b/framework/libra-framework/sources/ol_sources/ancestry.move @@ -174,7 +174,7 @@ module ol_framework::ancestry { // now loop through all the accounts again, and check if this target // account is related to anyone. - let k = 0; + let k = 0; while (k < vector::length
(&list)) { let comparison_acc = vector::borrow(&list, k); // skip if you're the same person diff --git a/framework/libra-framework/sources/ol_sources/epoch_boundary.move b/framework/libra-framework/sources/ol_sources/epoch_boundary.move index b8df79f4f..ba93fa84c 100644 --- a/framework/libra-framework/sources/ol_sources/epoch_boundary.move +++ b/framework/libra-framework/sources/ol_sources/epoch_boundary.move @@ -1,382 +1,386 @@ module diem_framework::epoch_boundary { - use ol_framework::slow_wallet; - use ol_framework::musical_chairs; - use ol_framework::proof_of_fee; - use ol_framework::stake; - use ol_framework::libra_coin::LibraCoin; - use ol_framework::rewards; - use ol_framework::jail; - use ol_framework::safe; - use ol_framework::burn; - use ol_framework::donor_voice_txs; - use ol_framework::fee_maker; - use ol_framework::infra_escrow; - use ol_framework::libra_coin; - use ol_framework::match_index; - use ol_framework::community_wallet_init; - use ol_framework::testnet; - - use diem_framework::account; - use diem_framework::reconfiguration; - use diem_framework::transaction_fee; - use diem_framework::system_addresses; - use diem_framework::coin::{Coin}; - use std::vector; - use std::error; - use std::string; - use std::signer; - use diem_framework::create_signer; - - use diem_std::debug::print; - - friend diem_framework::genesis; - friend diem_framework::diem_governance; - friend diem_framework::block; // for testnet only - - //////// ERRORS //////// - /// The transaction fee coin has not been initialized - const ETX_FEES_NOT_INITIALIZED: u64 = 0; - - /// Epoch trigger can only be called on mainnet or in smoketests - const ETRIGGER_EPOCH_UNAUTHORIZED: u64 = 1; - - /// Epoch is not ready for reconfiguration - const ETRIGGER_NOT_READY: u64 = 2; + use std::error; + use std::vector; + use std::string; + use std::signer; + use diem_framework::account; + use diem_framework::randomness; + use diem_framework::coin::{Coin}; + use diem_framework::create_signer; + use diem_framework::reconfiguration; + use diem_framework::transaction_fee; + use diem_framework::system_addresses; + use ol_framework::stake; + use ol_framework::jail; + use ol_framework::safe; + use ol_framework::burn; + use ol_framework::rewards; + use ol_framework::testnet; + use ol_framework::fee_maker; + use ol_framework::libra_coin; + use ol_framework::slow_wallet; + use ol_framework::match_index; + use ol_framework::proof_of_fee; + use ol_framework::infra_escrow; + use ol_framework::musical_chairs; + use ol_framework::donor_voice_txs; + use ol_framework::libra_coin::LibraCoin; + use ol_framework::community_wallet_init; + + use diem_std::debug::print; + + friend diem_framework::genesis; + friend diem_framework::diem_governance; + friend diem_framework::block; // for testnet only + + //////// ERRORS //////// + /// The transaction fee coin has not been initialized + const ETX_FEES_NOT_INITIALIZED: u64 = 0; + /// Epoch trigger can only be called on mainnet or in smoketests + const ETRIGGER_EPOCH_UNAUTHORIZED: u64 = 1; + /// Epoch is not ready for reconfiguration + const ETRIGGER_NOT_READY: u64 = 2; + /// Epoch number mismat + const ENOT_SAME_EPOCH: u64 = 3; + + /////// Constants //////// + /// How many PoF baseline rewards to we set aside for the miners. + /// equivalent reward of one seats of the validator set + const ORACLE_PROVIDERS_SEATS: u64 = 1; + + /// The VM can set the boundary bit to allow reconfiguration + struct BoundaryBit has key { + ready: bool, + closing_epoch: u64, + } - /// Epoch number mismat - const ENOT_SAME_EPOCH: u64 = 3; + /// I just checked in, to see what condition my condition was in. + struct BoundaryStatus has key, drop { + security_bill_count: u64, + security_bill_amount: u64, + security_bill_success: bool, + + dd_accounts_count: u64, + dd_accounts_amount: u64, + dd_accounts_success: bool, + + set_fee_makers_success: bool, + system_fees_collected: u64, + // Process Outgoing + outgoing_vals_paid: vector
, + outgoing_total_reward: u64, + outgoing_nominal_reward_to_vals: u64, + outgoing_entry_fee: u64, + outgoing_clearing_percent: u64, + outgoing_vals_success: bool, // TODO + + // Oracle / Tower + tower_state_success: bool, // TODO + oracle_budget: u64, + oracle_pay_count: u64, + oracle_pay_amount: u64, + oracle_pay_success: bool, + + epoch_burn_fees: u64, + epoch_burn_success: bool, + + slow_wallet_drip_amount: u64, + slow_wallet_drip_success: bool, + // Process Incoming + // musical chairs + incoming_compliant: vector
, + incoming_compliant_count: u64, + incoming_seats_offered: u64, + + // proof of fee + incoming_all_bidders: vector
, + incoming_only_qualified_bidders: vector
, + incoming_auction_winners: vector
, + incoming_filled_seats: u64, + incoming_fees: u64, + incoming_fees_success: bool, + + // reconfiguration + incoming_post_failover_check: vector
, + incoming_vals_missing_configs: vector
, + incoming_actual_vals: vector
, + incoming_final_set_size: u64, + incoming_reconfig_success: bool, + + infra_subsidize_amount: u64, // TODO + infra_subsidize_success: bool, // TODO + + pof_thermo_success: bool, + pof_thermo_increase: bool, + pof_thermo_amount: u64, + } - /////// Constants //////// - /// how many PoF baseline rewards to we set aside for the miners. - /// equivalent reward of one seats of the validator set - const ORACLE_PROVIDERS_SEATS: u64 = 1; + /// initialize structs, requires both signers since BoundaryBit can only be + // accessed by VM + public(friend) fun initialize(framework_signer: &signer) { + if (!exists(@ol_framework)){ + move_to(framework_signer, reset()); + }; - // the VM can set the boundary bit to allow reconfiguration - struct BoundaryBit has key { - ready: bool, - closing_epoch: u64, + // boundary bit can only be written by VM + if (!exists(@ol_framework)) { + move_to(framework_signer, BoundaryBit { + ready: false, + closing_epoch: 0, + }); } + } - // I just checked in, to see what condition my condition was in. - struct BoundaryStatus has key, drop { - security_bill_count: u64, - security_bill_amount: u64, - security_bill_success: bool, + fun reset(): BoundaryStatus { + BoundaryStatus { + security_bill_count: 0, + security_bill_amount: 0, + security_bill_success: false, + + dd_accounts_count: 0, + dd_accounts_amount: 0, + dd_accounts_success: false, - dd_accounts_count: u64, - dd_accounts_amount: u64, - dd_accounts_success: bool, + set_fee_makers_success: false, + system_fees_collected: 0, - set_fee_makers_success: bool, - system_fees_collected: u64, // Process Outgoing - outgoing_vals_paid: vector
, - outgoing_total_reward: u64, - outgoing_nominal_reward_to_vals: u64, - outgoing_entry_fee: u64, - outgoing_clearing_percent: u64, - outgoing_vals_success: bool, // TODO + outgoing_vals_paid: vector::empty(), + outgoing_total_reward: 0, + outgoing_vals_success: false, + outgoing_nominal_reward_to_vals: 0, + outgoing_entry_fee: 0, + outgoing_clearing_percent: 0, // Oracle / Tower - tower_state_success: bool, // TODO - oracle_budget: u64, - oracle_pay_count: u64, - oracle_pay_amount: u64, - oracle_pay_success: bool, - - epoch_burn_fees: u64, - epoch_burn_success: bool, - - slow_wallet_drip_amount: u64, - slow_wallet_drip_success: bool, + tower_state_success: false, + oracle_budget: 0, + oracle_pay_count: 0, + oracle_pay_amount: 0, + oracle_pay_success: false, + epoch_burn_fees: 0, + epoch_burn_success: false, + + slow_wallet_drip_amount: 0, + slow_wallet_drip_success: false, // Process Incoming - // musical chairs - incoming_compliant: vector
, - incoming_compliant_count: u64, - incoming_seats_offered: u64, - - // proof of fee - incoming_all_bidders: vector
, - incoming_only_qualified_bidders: vector
, - incoming_auction_winners: vector
, - incoming_filled_seats: u64, - incoming_fees: u64, - incoming_fees_success: bool, - - // reconfiguration - incoming_post_failover_check: vector
, - incoming_vals_missing_configs: vector
, - incoming_actual_vals: vector
, - incoming_final_set_size: u64, - incoming_reconfig_success: bool, - - infra_subsidize_amount: u64, // TODO - infra_subsidize_success: bool, // TODO - - pof_thermo_success: bool, - pof_thermo_increase: bool, - pof_thermo_amount: u64, - } - - /// initialize structs, requires both signers since BoundaryBit can only be - // accessed by VM - public(friend) fun initialize(framework_signer: &signer) { - if (!exists(@ol_framework)){ - move_to(framework_signer, reset()); - }; - - // boundary bit can only be written by VM - if (!exists(@ol_framework)) { - move_to(framework_signer, BoundaryBit { - ready: false, - closing_epoch: 0, - }); - } + incoming_compliant: vector::empty(), + incoming_compliant_count: 0, + incoming_seats_offered: 0, + incoming_filled_seats: 0, + incoming_auction_winners: vector::empty(), + incoming_all_bidders: vector::empty(), + incoming_only_qualified_bidders: vector::empty(), + incoming_final_set_size: 0, + incoming_fees: 0, + incoming_fees_success: false, + + incoming_post_failover_check: vector::empty(), + incoming_vals_missing_configs: vector::empty(), + incoming_actual_vals: vector::empty(), + incoming_reconfig_success: false, + + infra_subsidize_amount: 0, + infra_subsidize_success: false, + + pof_thermo_success: false, + pof_thermo_increase: false, + pof_thermo_amount: 0, } + } - fun reset(): BoundaryStatus { - BoundaryStatus { - security_bill_count: 0, - security_bill_amount: 0, - security_bill_success: false, - - dd_accounts_count: 0, - dd_accounts_amount: 0, - dd_accounts_success: false, - - set_fee_makers_success: false, - system_fees_collected: 0, - - // Process Outgoing - outgoing_vals_paid: vector::empty(), - outgoing_total_reward: 0, - outgoing_vals_success: false, - outgoing_nominal_reward_to_vals: 0, - outgoing_entry_fee: 0, - outgoing_clearing_percent: 0, - - // Oracle / Tower - tower_state_success: false, - oracle_budget: 0, - oracle_pay_count: 0, - oracle_pay_amount: 0, - oracle_pay_success: false, - epoch_burn_fees: 0, - epoch_burn_success: false, - - slow_wallet_drip_amount: 0, - slow_wallet_drip_success: false, - // Process Incoming - incoming_compliant: vector::empty(), - incoming_compliant_count: 0, - incoming_seats_offered: 0, - incoming_filled_seats: 0, - incoming_auction_winners: vector::empty(), - incoming_all_bidders: vector::empty(), - incoming_only_qualified_bidders: vector::empty(), - incoming_final_set_size: 0, - incoming_fees: 0, - incoming_fees_success: false, - - incoming_post_failover_check: vector::empty(), - incoming_vals_missing_configs: vector::empty(), - incoming_actual_vals: vector::empty(), - incoming_reconfig_success: false, - - infra_subsidize_amount: 0, - infra_subsidize_success: false, - - pof_thermo_success: false, - pof_thermo_increase: false, - pof_thermo_amount: 0, - } + ///TODO: epoch trigger is currently disabled and requires further testing. + /// refer to block.move and std::features + /// flip the bit to allow the epoch to be reconfigured on any transaction + public(friend) fun enable_epoch_trigger(vm_signer: &signer, closing_epoch: + u64) acquires BoundaryBit { + if (signer::address_of(vm_signer) != @vm_reserved) return; + + // though the VM is calling this, we need the struct to be on + // diem_framework. So we need to use create_signer + + let framework_signer = create_signer::create_signer(@ol_framework); + if (!exists(@ol_framework)) { + // Just like a prayer, your voice can take me there + // Just like a muse to me, you are a mystery + // Just like a dream, you are not what you seem + // Just like a prayer, no choice your voice can take me there... + move_to(&framework_signer, BoundaryBit { + closing_epoch: closing_epoch, + ready: true, + }) + } else { + let state = borrow_global_mut(@ol_framework); + state.closing_epoch = closing_epoch; + state.ready = true; } + } - ///TODO: epoch trigger is currently disabled and requires further testing. - /// refer to block.move and std::features - /// flip the bit to allow the epoch to be reconfigured on any transaction - public(friend) fun enable_epoch_trigger(vm_signer: &signer, closing_epoch: - u64) acquires BoundaryBit { - if (signer::address_of(vm_signer) != @vm_reserved) return; - - // though the VM is calling this, we need the struct to be on - // diem_framework. So we need to use create_signer - - let framework_signer = create_signer::create_signer(@ol_framework); - if (!exists(@ol_framework)) { - // Just like a prayer, your voice can take me there - // Just like a muse to me, you are a mystery - // Just like a dream, you are not what you seem - // Just like a prayer, no choice your voice can take me there... - move_to(&framework_signer, BoundaryBit { - closing_epoch: closing_epoch, - ready: true, - }) - } else { - let state = borrow_global_mut(@ol_framework); - state.closing_epoch = closing_epoch; - state.ready = true; - } - } - /// Once epoch boundary time has passed, and the BoundaryBit set to true - /// any user can trigger the epoch boundary. - /// Why do this? It's preferable that the VM never trigger any function. - /// An abort by the VM will cause a network halt. The same abort, if called - /// by a user, would not cause a halt. - public(friend) fun trigger_epoch(framework_signer: &signer) acquires BoundaryBit, - BoundaryStatus { - // COMMIT NOTE: there's no reason to gate this, if th trigger is not - // ready (which only happens on Main and Stage, then user will get an error) - // assert!(!testnet::is_testnet(), ETRIGGER_EPOCH_MAINNET); - // must get root permission from governance.move - system_addresses::assert_diem_framework(framework_signer); - let _ = can_trigger(); // will abort if false - - // update the state and flip the Bit - // note we are using the 0x0 address for BoundaryBit - let state = borrow_global_mut(@ol_framework); - state.ready = false; + /// Once epoch boundary time has passed, and the BoundaryBit set to true + /// any user can trigger the epoch boundary. + /// Why do this? It's preferable that the VM never trigger any function. + /// An abort by the VM will cause a network halt. The same abort, if called + /// by a user, would not cause a halt. + public(friend) fun trigger_epoch(framework_signer: &signer) + acquires BoundaryBit, BoundaryStatus { + // COMMIT NOTE: there's no reason to gate this, if th trigger is not + // ready (which only happens on Main and Stage, then user will get an error) + // assert!(!testnet::is_testnet(), ETRIGGER_EPOCH_MAINNET); + // must get root permission from governance.move + system_addresses::assert_diem_framework(framework_signer); + let _ = can_trigger(); // will abort if false + + // update the state and flip the Bit + // note we are using the 0x0 address for BoundaryBit + let state = borrow_global_mut(@ol_framework); + state.ready = false; + + epoch_boundary(framework_signer, state.closing_epoch, 0); + } - epoch_boundary(framework_signer, state.closing_epoch, 0); - } + // utility to use in smoke tests + public entry fun smoke_trigger_epoch(framework_signer: &signer) + acquires BoundaryBit, BoundaryStatus { + // cannot call this on mainnet + // only for smoke testing + assert!(testnet::is_not_mainnet(), ETRIGGER_EPOCH_UNAUTHORIZED); + // must get 0x1 sig from governance.move + system_addresses::assert_diem_framework(framework_signer); + let state = borrow_global_mut(@ol_framework); + epoch_boundary(framework_signer, state.closing_epoch, 0); + } - // utility to use in smoke tests - public entry fun smoke_trigger_epoch(framework_signer: &signer) acquires BoundaryBit, - BoundaryStatus { - // cannot call this on mainnet - // only for smoke testing - assert!(testnet::is_not_mainnet(), ETRIGGER_EPOCH_UNAUTHORIZED); - // must get 0x1 sig from governance.move - system_addresses::assert_diem_framework(framework_signer); - let state = borrow_global_mut(@ol_framework); - epoch_boundary(framework_signer, state.closing_epoch, 0); - } + #[view] + /// check to see if the epoch Boundary Bit is true + public fun can_trigger(): bool acquires BoundaryBit { + let state = borrow_global_mut(@ol_framework); + assert!(state.ready, ETRIGGER_NOT_READY); + assert!(state.closing_epoch == reconfiguration::get_current_epoch(), + ENOT_SAME_EPOCH); + true + } - #[view] - /// check to see if the epoch Boundary Bit is true - public fun can_trigger(): bool acquires BoundaryBit { - let state = borrow_global_mut(@ol_framework); - assert!(state.ready, ETRIGGER_NOT_READY); - assert!(state.closing_epoch == reconfiguration::get_current_epoch(), - ENOT_SAME_EPOCH); - true - } + // This function handles the necessary migrations that occur at the epoch boundary + // when new modules or structures are added by chain upgrades. + fun migrate_data(root: &signer) { + randomness::initialize(root); + } - // Contains all of 0L's business logic for end of epoch. - // This removed business logic from reconfiguration.move - // and prevents dependency cycling. - public(friend) fun epoch_boundary(root: &signer, closing_epoch: u64, - epoch_round: u64) acquires BoundaryStatus { - print(&string::utf8(b"EPOCH BOUNDARY BEGINS")); - // either 0x0 or 0x1 can call, but we will always use framework signer - system_addresses::assert_ol(root); - let root = &create_signer::create_signer(@ol_framework); - let status = borrow_global_mut(@ol_framework); - - print(&string::utf8(b"status reset")); - *status = reset(); - - // bill root service fees; - print(&string::utf8(b"root_service_billing")); - root_service_billing(root, status); - - print(&string::utf8(b"process_donor_voice_accounts")); - // run the transactions of donor directed accounts - let (count, amount, success) = donor_voice_txs::process_donor_voice_accounts(root, closing_epoch); - status.dd_accounts_count = count; - status.dd_accounts_amount = amount; - status.dd_accounts_success = success; - - print(&string::utf8(b"tower_state::reconfig")); - // reset fee makers tracking - status.set_fee_makers_success = fee_maker::epoch_reset_fee_maker(root); - - print(&string::utf8(b"musical_chairs::stop_the_music")); - let (compliant_vals, n_seats) = musical_chairs::stop_the_music(root, - closing_epoch, epoch_round); - status.incoming_compliant_count = vector::length(&compliant_vals); - status.incoming_compliant = compliant_vals; - status.incoming_seats_offered = n_seats; - - print(&string::utf8(b"settle_accounts")); - - settle_accounts(root, compliant_vals, status); - - print(&string::utf8(b"slow_wallet::on_new_epoch")); - // drip coins - let (s_success, s_amount) = slow_wallet::on_new_epoch(root); - - status.slow_wallet_drip_amount = s_amount; - status.slow_wallet_drip_success = s_success; - - // ======= THE BOUNDARY ======= - // And to know yourself - // is to be yourself - // keeps you walking through these tears. - - print(&string::utf8(b"process_incoming_validators")); - process_incoming_validators(root, status, compliant_vals, n_seats); - - print(&string::utf8(b"subsidize_from_infra_escrow")); - let (i_success, i_fee) = subsidize_from_infra_escrow(root); - status.infra_subsidize_amount = i_fee; - status.infra_subsidize_success = i_success; - - print(&string::utf8(b"reward_thermostat")); - let (t_success, t_increase, t_amount) = - proof_of_fee::reward_thermostat(root); - status.pof_thermo_success = t_success; - status.pof_thermo_increase = t_increase; - status.pof_thermo_amount = t_amount; - - print(&string::utf8(b"EPOCH BOUNDARY END")); - print(status); - reconfiguration::reconfigure(); + // Contains all of 0L's business logic for end of epoch. + // This removed business logic from reconfiguration.move + // and prevents dependency cycling. + public(friend) fun epoch_boundary(root: &signer, closing_epoch: u64, epoch_round: u64) + acquires BoundaryStatus { + print(&string::utf8(b"EPOCH BOUNDARY BEGINS")); + // either 0x0 or 0x1 can call, but we will always use framework signer + system_addresses::assert_ol(root); + let root = &create_signer::create_signer(@ol_framework); + let status = borrow_global_mut(@ol_framework); + + print(&string::utf8(b"migrate_data")); + migrate_data(root); + + print(&string::utf8(b"status reset")); + *status = reset(); + + // bill root service fees; + print(&string::utf8(b"root_service_billing")); + root_service_billing(root, status); + + print(&string::utf8(b"process_donor_voice_accounts")); + // run the transactions of donor directed accounts + let (count, amount, success) = donor_voice_txs::process_donor_voice_accounts(root, closing_epoch); + status.dd_accounts_count = count; + status.dd_accounts_amount = amount; + status.dd_accounts_success = success; + + print(&string::utf8(b"tower_state::reconfig")); + // reset fee makers tracking + status.set_fee_makers_success = fee_maker::epoch_reset_fee_maker(root); + + print(&string::utf8(b"musical_chairs::stop_the_music")); + let (compliant_vals, n_seats) = musical_chairs::stop_the_music(root, + closing_epoch, epoch_round); + status.incoming_compliant_count = vector::length(&compliant_vals); + status.incoming_compliant = compliant_vals; + status.incoming_seats_offered = n_seats; + + print(&string::utf8(b"settle_accounts")); + settle_accounts(root, compliant_vals, status); + + print(&string::utf8(b"slow_wallet::on_new_epoch")); + // drip coins + let (s_success, s_amount) = slow_wallet::on_new_epoch(root); + status.slow_wallet_drip_amount = s_amount; + status.slow_wallet_drip_success = s_success; + + // ======= THE BOUNDARY ======= + // And to know yourself + // is to be yourself + // keeps you walking through these tears. + + print(&string::utf8(b"process_incoming_validators")); + process_incoming_validators(root, status, compliant_vals, n_seats); + + print(&string::utf8(b"reward_thermostat")); + let (t_success, t_increase, t_amount) = + proof_of_fee::reward_thermostat(root); + status.pof_thermo_success = t_success; + status.pof_thermo_increase = t_increase; + status.pof_thermo_amount = t_amount; + + print(&string::utf8(b"subsidize_from_infra_escrow")); + let (i_success, i_fee) = subsidize_from_infra_escrow(root); + status.infra_subsidize_amount = i_fee; + status.infra_subsidize_success = i_success; + + print(&string::utf8(b"EPOCH BOUNDARY END")); + print(status); + reconfiguration::reconfigure(); } /// withdraw coins and settle accounts for validators and oracles /// returns the list of compliant_vals fun settle_accounts(root: &signer, compliant_vals: vector
, status: &mut BoundaryStatus): vector
{ - assert!(transaction_fee::is_fees_collection_enabled(), error::invalid_state(ETX_FEES_NOT_INITIALIZED)); - - if (transaction_fee::system_fees_collected() > 0) { - let all_fees = transaction_fee::root_withdraw_all(root); - status.system_fees_collected = libra_coin::value(&all_fees); - - // Nominal fee set by the PoF thermostat - let (nominal_reward_to_vals, entry_fee, clearing_percent, _ ) = proof_of_fee::get_consensus_reward(); - status.outgoing_nominal_reward_to_vals = nominal_reward_to_vals; - status.outgoing_entry_fee = entry_fee; - status.outgoing_clearing_percent = clearing_percent; - - // validators get the gross amount of the reward, since they already paid to enter. This results in a net payment equivalent to: - // nominal_reward_to_vals - entry_fee. - let (compliant_vals, total_reward) = process_outgoing_validators(root, &mut all_fees, nominal_reward_to_vals, compliant_vals); - - status.outgoing_vals_paid = compliant_vals; - status.outgoing_total_reward = total_reward; - - // check that the sustem fees collect were greater than reward - status.outgoing_vals_success = (status.system_fees_collected >= total_reward); - // check the the total actually deposited/paid is the expected amount - if (nominal_reward_to_vals > 0) { // check for zero - status.outgoing_vals_success = total_reward == (vector::length(&compliant_vals) * nominal_reward_to_vals) - }; - - // Commit note: deprecated with tower mining. - - // remainder gets burnt according to fee maker preferences - let (b_success, b_fees) = burn::epoch_burn_fees(root, &mut all_fees); - status.epoch_burn_success = b_success; - status.epoch_burn_fees = b_fees; - - // coin can finally be destroyed. Up to here we have been extracting from a mutable. - // It's possible there might be some dust, that should get burned - burn::burn_and_track(all_fees); - }; + assert!(transaction_fee::is_fees_collection_enabled(), error::invalid_state(ETX_FEES_NOT_INITIALIZED)); + + if (transaction_fee::system_fees_collected() > 0) { + let all_fees = transaction_fee::root_withdraw_all(root); + status.system_fees_collected = libra_coin::value(&all_fees); + + // Nominal fee set by the PoF thermostat + let (nominal_reward_to_vals, entry_fee, clearing_percent, _ ) = proof_of_fee::get_consensus_reward(); + status.outgoing_nominal_reward_to_vals = nominal_reward_to_vals; + status.outgoing_entry_fee = entry_fee; + status.outgoing_clearing_percent = clearing_percent; + + // validators get the gross amount of the reward, since they already paid to enter. This results in a net payment equivalent to: + // nominal_reward_to_vals - entry_fee. + let (compliant_vals, total_reward) = process_outgoing_validators(root, &mut all_fees, nominal_reward_to_vals, compliant_vals); + status.outgoing_vals_paid = compliant_vals; + status.outgoing_total_reward = total_reward; + + // check that the system fees collect were greater than reward + status.outgoing_vals_success = (status.system_fees_collected >= total_reward); + // check the the total actually deposited/paid is the expected amount + if (nominal_reward_to_vals > 0) { // check for zero + status.outgoing_vals_success = total_reward == (vector::length(&compliant_vals) * nominal_reward_to_vals) + }; - compliant_vals + // Commit note: deprecated with tower mining. + + // remainder gets burnt according to fee maker preferences + let (b_success, b_fees) = burn::epoch_burn_fees(root, &mut all_fees); + status.epoch_burn_success = b_success; + status.epoch_burn_fees = b_fees; + + // coin can finally be destroyed. Up to here we have been extracting from a mutable. + // It's possible there might be some dust, that should get burned + burn::burn_and_track(all_fees); + }; + + compliant_vals } @@ -403,11 +407,11 @@ module diem_framework::epoch_boundary { jail::jail(root, *addr); } else { // vector::push_back(&mut compliant_vals, *addr); - if (libra_coin::value(reward_budget) > reward_per) { + if (libra_coin::value(reward_budget) >= reward_per) { let user_coin = libra_coin::extract(reward_budget, reward_per); reward_deposited = reward_deposited + libra_coin::value(&user_coin); rewards::process_single(root, *addr, user_coin, 1); - } + }; }; i = i + 1; @@ -427,7 +431,6 @@ module diem_framework::epoch_boundary { status.incoming_only_qualified_bidders = only_qualified_bidders; status.incoming_auction_winners = auction_winners; - let post_failover_check = stake::check_failover_rules(auction_winners, n_seats); status.incoming_post_failover_check = post_failover_check; @@ -449,25 +452,22 @@ module diem_framework::epoch_boundary { // set up rewards subsidy for coming epoch fun subsidize_from_infra_escrow(root: &signer): (bool, u64) { - system_addresses::assert_ol(root); - let (reward_per, _, _, _ ) = proof_of_fee::get_consensus_reward(); - let vals = stake::get_current_validators(); - let count_vals = vector::length(&vals); - count_vals = count_vals + ORACLE_PROVIDERS_SEATS; - let total_epoch_budget = count_vals * reward_per; - infra_escrow::epoch_boundary_collection(root, - total_epoch_budget) + system_addresses::assert_ol(root); + let (reward_per, _, _, _ ) = proof_of_fee::get_consensus_reward(); + let vals = stake::get_current_validators(); + let count_vals = vector::length(&vals); + let total_epoch_budget = (count_vals * reward_per) + 1; // +1 for rounding + infra_escrow::epoch_boundary_collection(root, total_epoch_budget) } - /// check qualifications of community wallets - /// need to check every epoch so that wallets who no longer qualify are not biasing the Match algorithm. - fun reset_match_index_ratios(root: &signer) { - system_addresses::assert_ol(root); - let list = match_index::get_address_list(); - let good = community_wallet_init::get_qualifying(list); - match_index::calc_ratios(root, good); - } - + /// check qualifications of community wallets + /// need to check every epoch so that wallets who no longer qualify are not biasing the Match algorithm. + fun reset_match_index_ratios(root: &signer) { + system_addresses::assert_ol(root); + let list = match_index::get_address_list(); + let good = community_wallet_init::get_qualifying(list); + match_index::calc_ratios(root, good); + } // all services the root collective security is billing for fun root_service_billing(vm: &signer, status: &mut BoundaryStatus) { @@ -505,25 +505,27 @@ module diem_framework::epoch_boundary { #[test_only] public fun ol_reconfigure_for_test(vm: &signer, closing_epoch: u64, epoch_round: u64) acquires BoundaryStatus { - use diem_framework::system_addresses; + use diem_framework::system_addresses; + use diem_framework::randomness; - system_addresses::assert_ol(vm); - epoch_boundary(vm, closing_epoch, epoch_round); + system_addresses::assert_ol(vm); + randomness::initialize_for_testing(vm); + epoch_boundary(vm, closing_epoch, epoch_round); } #[test_only] public fun test_set_boundary_ready(framework: &signer, closing_epoch: u64) acquires BoundaryBit { - system_addresses::assert_ol(framework); - // don't check for "testnet" here, otherwise we can't test the production - // settings - let vm_signer = create_signer::create_signer(@vm_reserved); - enable_epoch_trigger(&vm_signer, closing_epoch); + system_addresses::assert_ol(framework); + // don't check for "testnet" here, otherwise we can't test the production + // settings + let vm_signer = create_signer::create_signer(@vm_reserved); + enable_epoch_trigger(&vm_signer, closing_epoch); } #[test_only] public fun test_trigger(vm: &signer) acquires BoundaryStatus, BoundaryBit { - // don't check for "testnet" here, otherwise we can't test the production settings - trigger_epoch(vm); + // don't check for "testnet" here, otherwise we can't test the production settings + trigger_epoch(vm); } } diff --git a/framework/libra-framework/sources/ol_sources/infra_escrow.move b/framework/libra-framework/sources/ol_sources/infra_escrow.move index c41061c5a..b91fbd34d 100644 --- a/framework/libra-framework/sources/ol_sources/infra_escrow.move +++ b/framework/libra-framework/sources/ol_sources/infra_escrow.move @@ -10,21 +10,21 @@ /////////////////////////////////////////////////////////////////////////// module ol_framework::infra_escrow{ + use std::error; use std::option::{Self, Option}; + use diem_framework::coin; + use diem_framework::transaction_fee; use diem_framework::system_addresses; + use ol_framework::ol_account; use ol_framework::libra_coin::LibraCoin; use ol_framework::pledge_accounts; // use ol_framework::slow_wallet; - use ol_framework::ol_account; - use diem_framework::coin; - use diem_framework::transaction_fee; // use std::fixed_point32; // use std::signer; - use std::error; // use diem_std::debug::print; - friend ol_framework::epoch_boundary; friend diem_framework::genesis; + friend ol_framework::epoch_boundary; const EGENESIS_REWARD: u64 = 0; /// for use on genesis, creates the infra escrow pledge policy struct @@ -71,7 +71,6 @@ module ol_framework::infra_escrow{ } - // Transaction script for user to pledge to infra escrow. fun user_pledge_infra(user_sig: &signer, amount: u64){ pledge_accounts::user_pledge(user_sig, @ol_framework, amount); diff --git a/framework/libra-framework/sources/ol_sources/jail.move b/framework/libra-framework/sources/ol_sources/jail.move index 8c32fd4a4..cde4d4e77 100644 --- a/framework/libra-framework/sources/ol_sources/jail.move +++ b/framework/libra-framework/sources/ol_sources/jail.move @@ -46,6 +46,7 @@ module ol_framework::jail { use std::error; use ol_framework::vouch; use ol_framework::stake; + use ol_framework::address_utils; friend ol_framework::validator_universe; friend ol_framework::epoch_boundary; @@ -157,31 +158,21 @@ module ol_framework::jail { }; } - /// gets a list of validators based on their jail reputation + /// Sort validators based on their jail reputation /// this is used in the bidding process for Proof-of-Fee where /// we seat the validators with the least amount of consecutive failures /// to rejoin. public(friend) fun sort_by_jail(vec_address: vector
): vector
acquires Jail { - - // Sorting the accounts vector based on value (weights). - // Bubble sort algorithm - let length = vector::length(&vec_address); - - let i = 0; - while (i < length){ - let j = 0; - while(j < length-i-1){ - - let (_, value_j) = get_jail_reputation(*vector::borrow(&vec_address, j)); - let (_, value_jp1) = get_jail_reputation(*vector::borrow(&vec_address, j + 1)); - - if(value_j > value_jp1){ - vector::swap
(&mut vec_address, j, j+1); - }; - j = j + 1; - }; - i = i + 1; - }; + // get the reputation of the validators + let reputation = vector::map(vec_address, |addr| { + let (_, value) = get_jail_reputation(addr); + value + }); + + // sort the validators based on their reputation + address_utils::sort_by_values(&mut vec_address, &mut reputation); + // shuffle duplicates scores to ensure fairness + address_utils::shuffle_duplicates(&mut vec_address, &reputation); vec_address } diff --git a/framework/libra-framework/sources/ol_sources/mock.move b/framework/libra-framework/sources/ol_sources/mock.move index b99e36079..40acbcecd 100644 --- a/framework/libra-framework/sources/ol_sources/mock.move +++ b/framework/libra-framework/sources/ol_sources/mock.move @@ -1,26 +1,28 @@ // Some fixtures are complex and are repeatedly needed #[test_only] module ol_framework::mock { + use std::vector; + use std::signer; + use diem_framework::coin; + use diem_framework::block; use diem_framework::stake; + use diem_framework::account; + use diem_framework::genesis; + use diem_framework::timestamp; use diem_framework::reconfiguration; + use diem_framework::system_addresses; + use diem_framework::transaction_fee; use ol_framework::grade; use ol_framework::vouch; - use std::vector; - use diem_framework::genesis; - use diem_framework::account; use ol_framework::slow_wallet; use ol_framework::proof_of_fee; use ol_framework::validator_universe; - use diem_framework::timestamp; - use diem_framework::system_addresses; use ol_framework::epoch_boundary; - use diem_framework::coin; use ol_framework::libra_coin::{Self, LibraCoin}; - use diem_framework::transaction_fee; use ol_framework::ol_account; use ol_framework::epoch_helper; use ol_framework::musical_chairs; - use diem_framework::block; + use ol_framework::pledge_accounts; // use diem_std::debug::print; @@ -36,265 +38,280 @@ module ol_framework::mock { #[test_only] public fun reset_val_perf_all(vm: &signer) { - let vals = stake::get_current_validators(); - let i = 0; - while (i < vector::length(&vals)) { - let a = vector::borrow(&vals, i); - stake::mock_performance(vm, *a, 0, 0); - i = i + 1; - }; + let vals = stake::get_current_validators(); + let i = 0; + while (i < vector::length(&vals)) { + let a = vector::borrow(&vals, i); + stake::mock_performance(vm, *a, 0, 0); + i = i + 1; + }; } #[test_only] - public fun mock_case_1(vm: &signer, addr: address){ - assert!(stake::is_valid(addr), 01); - stake::mock_performance(vm, addr, 100, 10); - let (compliant, _, _) = grade::get_validator_grade(addr); - assert!(compliant, 01); - } - - - #[test_only] - // did not do enough mining, but did validate. - public fun mock_case_4(vm: &signer, addr: address){ - assert!(stake::is_valid(addr), 01); - stake::mock_performance(vm, addr, 0, 100); // 100 failing proposals - // let (compliant, _, _, _) = grade::get_validator_grade(addr, 0); - let (compliant, _, _) = grade::get_validator_grade(addr); - assert!(!compliant, 02); - } - - // Mock all nodes being compliant case 1 - #[test_only] - public fun mock_all_vals_good_performance(vm: &signer) { - - let vals = stake::get_current_validators(); - - let i = 0; - while (i < vector::length(&vals)) { - - let a = vector::borrow(&vals, i); - mock_case_1(vm, *a); - i = i + 1; - }; - - } - - //////// PROOF OF FEE //////// - #[test_only] - public fun pof_default(): (vector
, vector, vector){ - - let vals = stake::get_current_validators(); - - let (bids, expiry) = mock_bids(&vals); - - // make all validators pay auction fee - // the clearing price in the fibonacci sequence is is 1 - let (alice_bid, _) = proof_of_fee::current_bid(*vector::borrow(&vals, 0)); - assert!(alice_bid == 1, 03); - (vals, bids, expiry) - } - - #[test_only] - public fun mock_bids(vals: &vector
): (vector, vector) { - // system_addresses::assert_ol(vm); - let bids = vector::empty(); - let expiry = vector::empty(); - let i = 0; - let prev = 0; - let fib = 1; - while (i < vector::length(vals)) { - - vector::push_back(&mut expiry, 1000); - let b = prev + fib; - vector::push_back(&mut bids, b); - - let a = vector::borrow(vals, i); - let sig = account::create_signer_for_test(*a); - // initialize and set. - proof_of_fee::pof_update_bid(&sig, b, 1000); - prev = fib; - fib = b; - i = i + 1; - }; - - (bids, expiry) - - } - - use diem_framework::chain_status; - #[test_only] - public fun ol_test_genesis(root: &signer) { - system_addresses::assert_ol(root); - genesis::setup(); - genesis::test_end_genesis(root); - - let mint_cap = init_coin_impl(root); - libra_coin::restore_mint_cap(root, mint_cap); - - assert!(!chain_status::is_genesis(), 0); - } - - #[test_only] - public fun ol_initialize_coin(root: &signer) { - system_addresses::assert_ol(root); - - let mint_cap = init_coin_impl(root); - - libra_coin::restore_mint_cap(root, mint_cap); - } - - #[test_only] - public fun ol_mint_to(root: &signer, addr: address, amount: u64) { - system_addresses::assert_ol(root); - - - let mint_cap = if (coin::is_coin_initialized()) { - libra_coin::extract_mint_cap(root) - } else { - init_coin_impl(root) - }; + public fun mock_case_1(vm: &signer, addr: address) { + assert!(stake::is_valid(addr), 01); + stake::mock_performance(vm, addr, 100, 10); + let (compliant, _, _) = grade::get_validator_grade(addr); + assert!(compliant, 01); + } + + + #[test_only] + // did not do enough mining, but did validate. + public fun mock_case_4(vm: &signer, addr: address) { + assert!(stake::is_valid(addr), 01); + stake::mock_performance(vm, addr, 0, 100); // 100 failing proposals + // let (compliant, _, _, _) = grade::get_validator_grade(addr, 0); + let (compliant, _, _) = grade::get_validator_grade(addr); + assert!(!compliant, 02); + } + + // Mock all nodes being compliant case 1 + #[test_only] + public fun mock_all_vals_good_performance(vm: &signer) { + let vals = stake::get_current_validators(); + let i = 0; + while (i < vector::length(&vals)) { + let a = vector::borrow(&vals, i); + mock_case_1(vm, *a); + i = i + 1; + }; + } + + // Mock some nodes not compliant + /*#[test_only] + public fun mock_some_vals_bad_performance(vm: &signer, bad_vals: u64) { + let vals = stake::get_current_validators(); + let vals_compliant = vector::length(&vals) - bad_vals; + let i = 0; + while (i < vals_compliant) { + let a = vector::borrow(&vals, i); + stake::mock_performance(vm, *a, 0, 100); // 100 failing proposals + i = i + 1; + }; + }*/ + + //////// PROOF OF FEE //////// + #[test_only] + public fun pof_default(): (vector
, vector, vector){ + + let vals = stake::get_current_validators(); + + let (bids, expiry) = mock_bids(&vals); + + // make all validators pay auction fee + // the clearing price in the fibonacci sequence is is 1 + let (alice_bid, _) = proof_of_fee::current_bid(*vector::borrow(&vals, 0)); + assert!(alice_bid == 1, 03); + (vals, bids, expiry) + } + + #[test_only] + public fun mock_bids(vals: &vector
): (vector, vector) { + // system_addresses::assert_ol(vm); + let bids = vector::empty(); + let expiry = vector::empty(); + let i = 0; + let prev = 0; + let fib = 1; + while (i < vector::length(vals)) { + + vector::push_back(&mut expiry, 1000); + let b = prev + fib; + vector::push_back(&mut bids, b); + + let a = vector::borrow(vals, i); + let sig = account::create_signer_for_test(*a); + // initialize and set. + proof_of_fee::pof_update_bid(&sig, b, 1000); + prev = fib; + fib = b; + i = i + 1; + }; + + (bids, expiry) + } + + use diem_framework::chain_status; + #[test_only] + public fun ol_test_genesis(root: &signer) { + system_addresses::assert_ol(root); + genesis::setup(); + genesis::test_end_genesis(root); + + let mint_cap = init_coin_impl(root); + libra_coin::restore_mint_cap(root, mint_cap); + + assert!(!chain_status::is_genesis(), 0); + } + + #[test_only] + public fun ol_initialize_coin(root: &signer) { + system_addresses::assert_ol(root); + + let mint_cap = init_coin_impl(root); + + libra_coin::restore_mint_cap(root, mint_cap); + } + + #[test_only] + public fun ol_mint_to(root: &signer, addr: address, amount: u64) { + system_addresses::assert_ol(root); + + let mint_cap = if (coin::is_coin_initialized()) { + libra_coin::extract_mint_cap(root) + } else { + init_coin_impl(root) + }; + + let c = coin::test_mint(amount, &mint_cap); + ol_account::deposit_coins(addr, c); + + let b = coin::balance(addr); + assert!(b == amount, 0001); + + libra_coin::restore_mint_cap(root, mint_cap); + } + + #[test_only] + public fun ol_initialize_coin_and_fund_vals(root: &signer, amount: u64, + drip: bool) { + system_addresses::assert_ol(root); + + let mint_cap = if (coin::is_coin_initialized()) { + libra_coin::extract_mint_cap(root) + } else { + init_coin_impl(root) + }; + + let vals = stake::get_current_validators(); + let i = 0; + while (i < vector::length(&vals)) { + let addr = vector::borrow(&vals, i); let c = coin::test_mint(amount, &mint_cap); - ol_account::deposit_coins(addr, c); + ol_account::deposit_coins(*addr, c); - let b = coin::balance(addr); + let b = libra_coin::balance(*addr); assert!(b == amount, 0001); - libra_coin::restore_mint_cap(root, mint_cap); - } - - #[test_only] - public fun ol_initialize_coin_and_fund_vals(root: &signer, amount: u64, - drip: bool) { - system_addresses::assert_ol(root); - - let mint_cap = if (coin::is_coin_initialized()) { - libra_coin::extract_mint_cap(root) - } else { - init_coin_impl(root) - }; - - let vals = stake::get_current_validators(); - let i = 0; - - while (i < vector::length(&vals)) { - let addr = vector::borrow(&vals, i); - let c = coin::test_mint(amount, &mint_cap); - ol_account::deposit_coins(*addr, c); - - let b = libra_coin::balance(*addr); - assert!(b == amount, 0001); - - - i = i + 1; - }; - - if (drip) { - slow_wallet::slow_wallet_epoch_drip(root, amount); - }; - libra_coin::restore_mint_cap(root, mint_cap); - } - - #[test_only] - fun init_coin_impl(root: &signer): coin::MintCapability { - system_addresses::assert_ol(root); - - let (burn_cap, mint_cap) = libra_coin::initialize_for_test_without_aggregator_factory(root); - coin::destroy_burn_cap(burn_cap); - - transaction_fee::initialize_fee_collection_and_distribution(root, 0); - - let initial_fees = 1000000 * 100; // coin scaling * 100 coins - let tx_fees = coin::test_mint(initial_fees, &mint_cap); - transaction_fee::vm_pay_fee(root, @ol_framework, tx_fees); - let supply_pre = libra_coin::supply(); - assert!(supply_pre == initial_fees, ESUPPLY_MISMATCH); - libra_coin::test_set_final_supply(root, initial_fees); - - mint_cap - } - - #[test_only] - public fun personas(): vector
{ - let val_addr = vector::empty
(); - - vector::push_back(&mut val_addr, @0x1000a); - vector::push_back(&mut val_addr, @0x1000b); - vector::push_back(&mut val_addr, @0x1000c); - vector::push_back(&mut val_addr, @0x1000d); - vector::push_back(&mut val_addr, @0x1000e); - vector::push_back(&mut val_addr, @0x1000f); - vector::push_back(&mut val_addr, @0x10010); // g - vector::push_back(&mut val_addr, @0x10011); // h - vector::push_back(&mut val_addr, @0x10012); // i - vector::push_back(&mut val_addr, @0x10013); // k - val_addr - } - - #[test_only] - /// mock up to 6 validators alice..frank - public fun genesis_n_vals(root: &signer, num: u64): vector
{ - system_addresses::assert_ol(root); - let framework_sig = account::create_signer_for_test(@diem_framework); - ol_test_genesis(&framework_sig); - // need to initialize musical chairs separate from genesis. - let musical_chairs_default_seats = 10; - musical_chairs::initialize(root, musical_chairs_default_seats); - - let val_addr = personas(); - let i = 0; - while (i < num) { - let val = vector::borrow(&val_addr, i); - let sig = account::create_signer_for_test(*val); - - let (_sk, pk, pop) = stake::generate_identity(); - - validator_universe::test_register_validator(root, &pk, &pop, &sig, 100, - true, true); - - vouch::init(&sig); - vouch::test_set_buddies(*val, val_addr); - - i = i + 1; - }; - - timestamp::fast_forward_seconds(2); // or else reconfigure wont happen - stake::test_reconfigure(root, validator_universe::get_eligible_validators()); - - stake::get_current_validators() - } - - #[test_only] - const EPOCH_DURATION: u64 = 60; - - #[test_only] - // NOTE: The order of these is very important. - // ol first runs its own accounting at end of epoch with epoch_boundary - // Then the stake module needs to update the validators. - // the reconfiguration module must run last, since no other - // transactions or operations can happen after the reconfig. - public fun trigger_epoch(root: &signer) { - trigger_epoch_exactly_at( - root, - reconfiguration::get_current_epoch(), - block::get_current_block_height() - ); - } - - public fun trigger_epoch_exactly_at(root: &signer, old_epoch: u64, round: u64) { - timestamp::fast_forward_seconds(EPOCH_DURATION); - - epoch_boundary::ol_reconfigure_for_test(root, old_epoch, round); - - // always advance - assert!(reconfiguration::get_current_epoch() > old_epoch, - EDID_NOT_ADVANCE_EPOCH); - - // epoch helper should always be in sync - assert!(reconfiguration::get_current_epoch() == - epoch_helper::get_current_epoch(), 666); - } + i = i + 1; + }; + + if (drip) { + slow_wallet::slow_wallet_epoch_drip(root, amount); + }; + libra_coin::restore_mint_cap(root, mint_cap); + } + + #[test_only] + fun init_coin_impl(root: &signer): coin::MintCapability { + system_addresses::assert_ol(root); + + let (burn_cap, mint_cap) = libra_coin::initialize_for_test_without_aggregator_factory(root); + coin::destroy_burn_cap(burn_cap); + + transaction_fee::initialize_fee_collection_and_distribution(root, 0); + + let initial_fees = 5_000_000 * 100; // coin scaling * 100 coins + let tx_fees = coin::test_mint(initial_fees, &mint_cap); + transaction_fee::vm_pay_fee(root, @ol_framework, tx_fees); + + // Forge Bruce + let fortune = 100_000_000_000; + let bruce_address = @0xBA7; + ol_account::create_account(root, bruce_address); + + // Bruce mints a fortune + let bruce = account::create_signer_for_test(bruce_address); + let fortune_mint = coin::test_mint(fortune, &mint_cap); + ol_account::deposit_coins(bruce_address, fortune_mint); + + // Bruce funds infra escrow + let framework = signer::address_of(root); + pledge_accounts::user_pledge(&bruce, framework, 37_000_000_000); + + let supply_pre = libra_coin::supply(); + assert!(supply_pre == (initial_fees + fortune), ESUPPLY_MISMATCH); + libra_coin::test_set_final_supply(root, initial_fees); + + mint_cap + } + + #[test_only] + public fun personas(): vector
{ + let val_addr = vector::empty
(); + vector::push_back(&mut val_addr, @0x1000a); + vector::push_back(&mut val_addr, @0x1000b); + vector::push_back(&mut val_addr, @0x1000c); + vector::push_back(&mut val_addr, @0x1000d); + vector::push_back(&mut val_addr, @0x1000e); + vector::push_back(&mut val_addr, @0x1000f); + vector::push_back(&mut val_addr, @0x10010); // g + vector::push_back(&mut val_addr, @0x10011); // h + vector::push_back(&mut val_addr, @0x10012); // i + vector::push_back(&mut val_addr, @0x10013); // k + val_addr + } + + #[test_only] + /// mock up to 6 validators alice..frank + public fun genesis_n_vals(root: &signer, num: u64): vector
{ + system_addresses::assert_ol(root); + let framework_sig = account::create_signer_for_test(@diem_framework); + ol_test_genesis(&framework_sig); + // need to initialize musical chairs separate from genesis. + let musical_chairs_default_seats = 10; + musical_chairs::initialize(root, musical_chairs_default_seats); + + let val_addr = personas(); + let i = 0; + while (i < num) { + let val = vector::borrow(&val_addr, i); + let sig = account::create_signer_for_test(*val); + let (_sk, pk, pop) = stake::generate_identity(); + validator_universe::test_register_validator(root, &pk, &pop, &sig, 100, true, true); + + vouch::init(&sig); + vouch::test_set_buddies(*val, val_addr); + + i = i + 1; + }; + + timestamp::fast_forward_seconds(2); // or else reconfigure wont happen + stake::test_reconfigure(root, validator_universe::get_eligible_validators()); + + stake::get_current_validators() + } + + #[test_only] + const EPOCH_DURATION: u64 = 60; + + #[test_only] + // NOTE: The order of these is very important. + // ol first runs its own accounting at end of epoch with epoch_boundary + // Then the stake module needs to update the validators. + // the reconfiguration module must run last, since no other + // transactions or operations can happen after the reconfig. + public fun trigger_epoch(root: &signer) { + trigger_epoch_exactly_at( + root, + reconfiguration::get_current_epoch(), + block::get_current_block_height() + ); + } + + public fun trigger_epoch_exactly_at(root: &signer, old_epoch: u64, round: u64) { + timestamp::fast_forward_seconds(EPOCH_DURATION); + epoch_boundary::ol_reconfigure_for_test(root, old_epoch, round); + + // always advance + assert!(reconfiguration::get_current_epoch() > old_epoch, + EDID_NOT_ADVANCE_EPOCH); + + // epoch helper should always be in sync + assert!(reconfiguration::get_current_epoch() == epoch_helper::get_current_epoch(), 666); + } //////// META TESTS //////// @@ -339,11 +356,12 @@ module ol_framework::mock { let n_vals = 5; let _vals = genesis_n_vals(root, n_vals); // need to include eve to init funds - let genesis_mint = 1000000; + let genesis_mint = 1_000_000; ol_initialize_coin_and_fund_vals(root, genesis_mint, true); let supply_pre = libra_coin::supply(); - let mocked_tx_fees = 1000000 * 100; - assert!(supply_pre == mocked_tx_fees + (n_vals * genesis_mint), 73570001); + let bruce_fortune = 100_000_000_000; + let mocked_tx_fees = 5_000_000 * 100; + assert!(supply_pre == bruce_fortune + mocked_tx_fees + (n_vals * genesis_mint), 73570001); } @@ -358,9 +376,9 @@ module ol_framework::mock { let (nominal_reward, entry_fee, clearing_percent, median_bid ) = proof_of_fee::get_consensus_reward(); - assert!(nominal_reward == 1000000, 73570001); + assert!(nominal_reward == 1_000_000, 73570001); assert!(clearing_percent == 1, 73570002); - assert!(entry_fee == 999, 73570003); + assert!(entry_fee == 1_000, 73570003); assert!(median_bid == 3, 73570004); } } diff --git a/framework/libra-framework/sources/ol_sources/musical_chairs.move b/framework/libra-framework/sources/ol_sources/musical_chairs.move index fee0eff82..8e3283a8b 100644 --- a/framework/libra-framework/sources/ol_sources/musical_chairs.move +++ b/framework/libra-framework/sources/ol_sources/musical_chairs.move @@ -1,240 +1,273 @@ module ol_framework::musical_chairs { - use diem_framework::chain_status; - use diem_framework::system_addresses; - use diem_framework::stake; - use ol_framework::grade; - use ol_framework::testnet; - use std::fixed_point32; - use std::vector; - - // use diem_std::debug::print; - - friend diem_framework::genesis; - friend diem_framework::diem_governance; - friend ol_framework::epoch_boundary; - #[test_only] - friend ol_framework::mock; - - - /// we don't want to play the validator selection games + use std::fixed_point32; + use std::vector; + use diem_framework::chain_status; + use diem_framework::system_addresses; + use diem_framework::stake; + use ol_framework::grade; + use ol_framework::testnet; + //use diem_std::debug::print; + + friend diem_framework::genesis; + friend diem_framework::diem_governance; + friend ol_framework::epoch_boundary; + #[test_only] + friend ol_framework::mock; + + /// We don't want to play the validator selection games /// before we're clear out of genesis const EPOCH_TO_START_EVAL: u64 = 2; - - /// we can't evaluate the performance of validators + /// We can't evaluate the performance of validators /// when there are too few rounds committed const MINIMUM_ROUNDS_PER_EPOCH: u64 = 1000; - - struct Chairs has key { - // The number of chairs in the game - seats_offered: u64, - // TODO: A small history, for future use. - history: vector, - } - - // With musical chairs we are trying to estimate - // the number of nodes which the network can support - // BFT has upperbounds in the low hundreds, but we - // don't need to hard code it. - // There also needs to be an upper bound so that there is some - // competition among validators. - // Instead of hard coding a number, and needing to reach social - // consensus to change it: we'll determine the size based on - // the network's performance as a whole. - // There are many metrics that can be used. For now we'll use - // a simple heuristic that is already on chain: compliant node cardinality. - // Other heuristics may be explored, so long as the information - // reliably committed to the chain. - - // The rules: - // Validators who perform, are not guaranteed entry into the - // next epoch of the chain. All we are establishing is the ceiling - // for validators. - // When the 100% of the validators are performing well - // the network can safely increase the threshold by 1 node. - // We can also say if less that 5% fail, no change happens. - // When the network is performing poorly, greater than 5%, - // the threshold is reduced not by a predetermined unit, but - // to the number of compliant and performant nodes. - - /// Called by root in genesis to initialize the GAS coin - public(friend) fun initialize( - vm: &signer, - genesis_seats: u64, - ) { - // system_addresses::assert_vm(vm); - // TODO: replace with VM - system_addresses::assert_ol(vm); - - chain_status::is_genesis(); - if (exists(@ol_framework)) { - return - }; - - move_to(vm, Chairs { - seats_offered: genesis_seats, - history: vector::empty(), - }); - } - - /// get the number of seats in the game - /// returns the list of compliant validators and the number of seats - /// we should offer in the next epoch - /// (compliant_vals, seats_offered) - public(friend) fun stop_the_music( // sorry, had to. - vm: &signer, - epoch: u64, - round: u64, - ): (vector
, u64) acquires Chairs { - system_addresses::assert_ol(vm); - - let validators = stake::get_current_validators(); - - let (compliant_vals, _non, fail_ratio) = - eval_compliance_impl(validators, epoch, round); - - let chairs = borrow_global_mut(@ol_framework); - - let num_compliant_nodes = vector::length(&compliant_vals); - - - // Error handle. We should not have gone into an epoch where we - // had MORE validators than seats offered. - // If this happens it's because we are in some kind of a fork condition. - // return with no changes - if (num_compliant_nodes > chairs.seats_offered) { - return (compliant_vals, chairs.seats_offered) - }; - - // The happiest case. All filled seats performed well in the last epoch - if (fixed_point32::is_zero(*&fail_ratio)) { // handle this here to prevent multiplication error below - chairs.seats_offered = chairs.seats_offered + 1; - return (compliant_vals, chairs.seats_offered) - }; - - // Conditions under which seats should be one more than the number of compliant nodes(<= 5%) - // Sad case. If we are not getting compliance, need to ratchet down the offer of seats in next epoch. - // See below find_safe_set_size, how we determine what that number - // should be - let non_compliance_pct = fixed_point32::multiply_u64(100, *&fail_ratio); - - if (non_compliance_pct > 5) { - // If network is bootstrapping don't reduce the seat count below - // compliant nodes, - - chairs.seats_offered = num_compliant_nodes; - - } else { - // Ok case. If it's between 0 and 5% then we accept that margin as if it was fully compliant - chairs.seats_offered = chairs.seats_offered + 1; - }; - - // catch failure mode - // mostly for genesis, or testnets - if (chairs.seats_offered < 4) { - chairs.seats_offered = 4; - }; - - (compliant_vals, chairs.seats_offered) - } - - // Update seat count to match filled seats post-PoF auction. - // in case we were not able to fill all the seats offered - // we don't want to keep incrementing from a baseline which we cannot fill - // it can spiral out of range. - public(friend) fun set_current_seats(framework: &signer, filled_seats: u64): u64 acquires Chairs{ - system_addresses::assert_diem_framework(framework); - let chairs = borrow_global_mut(@ol_framework); - chairs.seats_offered = filled_seats; + struct Chairs has key { + // The number of chairs in the game + seats_offered: u64, + // TODO: A small history, for future use. + history: vector, + } + + // With musical chairs we are trying to estimate + // the number of nodes which the network can support + // BFT has upperbounds in the low hundreds, but we + // don't need to hard code it. + // There also needs to be an upper bound so that there is some + // competition among validators. + // Instead of hard coding a number, and needing to reach social + // consensus to change it: we'll determine the size based on + // the network's performance as a whole. + // There are many metrics that can be used. For now we'll use + // a simple heuristic that is already on chain: compliant node cardinality. + // Other heuristics may be explored, so long as the information + // reliably committed to the chain. + + // The rules: + // Validators who perform, are not guaranteed entry into the + // next epoch of the chain. All we are establishing is the ceiling + // for validators. + // When the 100% of the validators are performing well + // the network can safely increase the threshold by 1 node. + // We can also say if less that 5% fail, no change happens. + // When the network is performing poorly, greater than 5%, + // the threshold is reduced not by a predetermined unit, but + // to the number of compliant and performant nodes. + + /// Called by root in genesis to initialize the GAS coin + public(friend) fun initialize( + vm: &signer, + genesis_seats: u64, + ) { + // system_addresses::assert_vm(vm); + // TODO: replace with VM + system_addresses::assert_ol(vm); + + chain_status::is_genesis(); + if (exists(@ol_framework)) { + return + }; + + move_to(vm, Chairs { + seats_offered: genesis_seats, + history: vector::empty(), + }); + } + + /// get the number of seats in the game + /// returns the list of compliant validators and the number of seats + /// we should offer in the next epoch + /// (compliant_vals, seats_offered) + public(friend) fun stop_the_music( + vm: &signer, + epoch: u64, + round: u64, + ): (vector
, u64) acquires Chairs { + system_addresses::assert_ol(vm); + + let chairs = borrow_global_mut(@ol_framework); + let validators = stake::get_current_validators(); + let (compliant_vals, _non, fail_ratio) = eval_compliance_impl(validators, epoch, round); + let num_compliant_vals = vector::length(&compliant_vals); + + // Error handle. We should not have gone into an epoch where we had MORE validators than seats offered. + // If this happens it's because we are in some kind of a fork condition. Return with no changes + if (num_compliant_vals > chairs.seats_offered) { + return (compliant_vals, chairs.seats_offered) + }; + + // Calculate the number of seats to offer + chairs.seats_offered = calculate_seats_to_offer( + num_compliant_vals, + fail_ratio, chairs.seats_offered + ); + + (compliant_vals, chairs.seats_offered) + } + + // Case: All seats performed well in the last epoch + // - The happiest case: All filled seats performed well in the last epoch. + // - Increment the number of seats offered by 1. + // + // Case: Non-compliance detected + // - If non-compliance is detected (> 5%), reduce the number of seats offered to the number of compliant nodes. + // - Otherwise, if non-compliance is between 0 and 5%, increment the number of seats offered by 1. + // + // Error Handling + // - If there are more validators than seats offered, it indicates a fork condition. In such a case, return with no changes. + // + // Bootstrap Mode + // - If the network is bootstrapping, ensure that the seat count does not fall below the number of compliant nodes. + // - Ensure that the number of seats offered is not less than 4, especially during genesis or in testnets. + + public fun calculate_seats_to_offer( + num_compliant_vals: u64, + fail_ratio: fixed_point32::FixedPoint32, + current_seats_offered: u64 + ): u64 { + let new_seats_offered = if (fixed_point32::is_zero(fail_ratio)) { + // The happiest case. All filled seats performed well in the last epoch + current_seats_offered + 1 + } else { + let non_compliance_pct = fixed_point32::multiply_u64(100, fail_ratio); + if (non_compliance_pct > 5) { + // Sad case. If we are not getting compliance, need to ratchet down the offer of seats in the next epoch. + // See below find_safe_set_size, how we determine what that number should be + num_compliant_vals + } else { + // Ok case. If it's between 0 and 5% then we accept that margin as if it was fully compliant + current_seats_offered + 1 + } + }; + + // Catch failure mode mostly for genesis, or testnets + if (new_seats_offered < 4) { + 4 + } else { + new_seats_offered } - - // use the Case statistic to determine what proportion of the network is compliant. - // private function prevent list DoS. - fun eval_compliance_impl( - validators: vector
, - epoch: u64, - _round: u64, - ): (vector
, vector
, fixed_point32::FixedPoint32) { - - let val_set_len = vector::length(&validators); - - // skip first epoch, don't evaluate if the network doesn't have sufficient data - if (epoch < EPOCH_TO_START_EVAL) return (validators, vector::empty
(), fixed_point32::create_from_rational(1, 1)); - - let (compliant_nodes, non_compliant_nodes) = grade::eval_compliant_vals(validators); - - let good_len = vector::length(&compliant_nodes) ; - let bad_len = vector::length(&non_compliant_nodes); - - // Note: sorry for repetition but necessary for writing tests and debugging. - let null = fixed_point32::create_from_raw_value(0); - if (good_len > val_set_len) { // safety - return (vector::empty(), vector::empty(), null) - }; - - if (bad_len > val_set_len) { // safety - return (vector::empty(), vector::empty(), null) - }; - - if ((good_len + bad_len) != val_set_len) { // safety - return (vector::empty(), vector::empty(), null) - }; - - let ratio = if (bad_len > 0) { - fixed_point32::create_from_rational(bad_len, val_set_len) - } else { - null - }; - - (compliant_nodes, non_compliant_nodes, ratio) - } - - // Check for genesis, upgrade or recovery mode scenarios - // if we are at genesis or otherwise at start of an epoch and don't - // have a sufficient amount of history to evaluate nodes - // we might reduce the validator set too agressively. - // Musical chairs should not evaluate performance with less than 1000 rounds - // created on mainnet, - // there's something else very wrong in that case. - - fun is_booting_up(epoch: u64, round: u64): bool { - !testnet::is_testnet() && - (epoch < EPOCH_TO_START_EVAL || - round < MINIMUM_ROUNDS_PER_EPOCH) - } - - //////// GETTERS //////// - - #[view] - public fun get_current_seats(): u64 acquires Chairs { - borrow_global(@ol_framework).seats_offered - } - - //////// TEST HELPERS //////// - - #[test_only] - use diem_framework::chain_id; - - #[test_only] - public fun test_stop(vm: &signer, epoch: u64, epoch_round: u64): (vector
, u64) acquires Chairs { - stop_the_music(vm, epoch, epoch_round) - } - - #[test_only] - public fun test_eval_compliance(root: &signer, validators: vector
, - epoch: u64, round: u64): (vector
, vector
, - fixed_point32::FixedPoint32) { - system_addresses::assert_ol(root); - eval_compliance_impl(validators, epoch, round) - - } - //////// TESTS //////// - - #[test(vm = @ol_framework)] - public entry fun initialize_chairs(vm: signer) acquires Chairs { - chain_id::initialize_for_test(&vm, 4); - initialize(&vm, 10); - assert!(get_current_seats() == 10, 1004); - } + } + + // Update seat count to match filled seats post-PoF auction. + // in case we were not able to fill all the seats offered + // we don't want to keep incrementing from a baseline which we cannot fill + // it can spiral out of range. + public(friend) fun set_current_seats(framework: &signer, filled_seats: u64): u64 acquires Chairs{ + system_addresses::assert_diem_framework(framework); + let chairs = borrow_global_mut(@ol_framework); + chairs.seats_offered = filled_seats; + chairs.seats_offered + } + + // use the Case statistic to determine what proportion of the network is compliant. + // private function prevent list DoS. + fun eval_compliance_impl( + validators: vector
, + epoch: u64, + _round: u64, + ): (vector
, vector
, fixed_point32::FixedPoint32) { + let val_set_len = vector::length(&validators); + + // skip first epoch, don't evaluate if the network doesn't have sufficient data + if (epoch < EPOCH_TO_START_EVAL) return (validators, vector::empty
(), fixed_point32::create_from_rational(1, 1)); + + let (compliant_nodes, non_compliant_nodes) = grade::eval_compliant_vals(validators); + + let good_len = vector::length(&compliant_nodes) ; + let bad_len = vector::length(&non_compliant_nodes); + + // Note: sorry for repetition but necessary for writing tests and debugging. + let null = fixed_point32::create_from_raw_value(0); + if (good_len > val_set_len) { // safety + return (vector::empty(), vector::empty(), null) + }; + + if (bad_len > val_set_len) { // safety + return (vector::empty(), vector::empty(), null) + }; + + if ((good_len + bad_len) != val_set_len) { // safety + return (vector::empty(), vector::empty(), null) + }; + + let ratio = if (bad_len > 0) { + fixed_point32::create_from_rational(bad_len, val_set_len) + } else { + null + }; + + (compliant_nodes, non_compliant_nodes, ratio) + } + + // Check for genesis, upgrade or recovery mode scenarios + // if we are at genesis or otherwise at start of an epoch and don't + // have a sufficient amount of history to evaluate nodes + // we might reduce the validator set too agressively. + // Musical chairs should not evaluate performance with less than 1000 rounds + // created on mainnet, + // there's something else very wrong in that case. + + fun is_booting_up(epoch: u64, round: u64): bool { + !testnet::is_testnet() && + (epoch < EPOCH_TO_START_EVAL || + round < MINIMUM_ROUNDS_PER_EPOCH) + } + + //////// GETTERS //////// + + #[view] + public fun get_current_seats(): u64 acquires Chairs { + borrow_global(@ol_framework).seats_offered + } + + //////// TEST HELPERS //////// + + #[test_only] + use diem_framework::chain_id; + + #[test_only] + public fun test_stop(vm: &signer, epoch: u64, epoch_round: u64): (vector
, u64) acquires Chairs { + stop_the_music(vm, epoch, epoch_round) + } + + #[test_only] + public fun test_eval_compliance(root: &signer, validators: vector
, + epoch: u64, round: u64): (vector
, vector
, + fixed_point32::FixedPoint32) { + system_addresses::assert_ol(root); + eval_compliance_impl(validators, epoch, round) + + } + + //////// TESTS //////// + + #[test(vm = @ol_framework)] + public entry fun initialize_chairs(vm: signer) acquires Chairs { + chain_id::initialize_for_test(&vm, 4); + initialize(&vm, 10); + assert!(get_current_seats() == 10, 1004); + } + + #[test] + fun test_calculate_seats_to_offer() { + // Test case: all seats are compliant + let result = calculate_seats_to_offer(5, fixed_point32::create_from_rational(0, 1), 5); + assert!(result == 6, 1001); + + // Test case: more than 5% non-compliance + let result = calculate_seats_to_offer(18, fixed_point32::create_from_rational(2, 20), 20); + assert!(result == 18, 1002); + + // Test case: 5% non-compliance + let result = calculate_seats_to_offer(19, fixed_point32::create_from_rational(1, 20), 20); + assert!(result == 21, 1003); + + // Test case: less than 5% non-compliance + let result = calculate_seats_to_offer(98, fixed_point32::create_from_rational(2, 100), 100); + assert!(result == 101, 1003); + + // Test case: seats offered should not be less than 4 + let result = calculate_seats_to_offer(2, fixed_point32::create_from_rational(1, 1), 2); + assert!(result == 4, 1004); + } } diff --git a/framework/libra-framework/sources/ol_sources/pledge_accounts.move b/framework/libra-framework/sources/ol_sources/pledge_accounts.move index 1aebfd115..c25cf387c 100644 --- a/framework/libra-framework/sources/ol_sources/pledge_accounts.move +++ b/framework/libra-framework/sources/ol_sources/pledge_accounts.move @@ -51,12 +51,15 @@ use diem_framework::coin; use diem_framework::system_addresses; - // use diem_std::debug::print; + //use diem_std::debug::print; friend ol_framework::infra_escrow; friend ol_framework::genesis_migration; friend ol_framework::genesis; + #[test_only] + friend ol_framework::mock; + /// no policy at this address const ENO_BENEFICIARY_POLICY: u64 = 1; /// there is a non zero balance @@ -160,7 +163,7 @@ sig: &signer, address_of_beneficiary: address, pledge: coin::Coin - ) acquires MyPledges, BeneficiaryPolicy { + ) acquires MyPledges, BeneficiaryPolicy { maybe_initialize_my_pledges(sig); assert!(exists(address_of_beneficiary), error::invalid_state(ENO_BENEFICIARY_POLICY)); let sender_addr = signer::address_of(sig); @@ -248,57 +251,56 @@ // withdraw an amount from all pledge accounts. Check first that there are remaining funds before attempting to withdraw. public(friend) fun withdraw_from_all_pledge_accounts(sig_beneficiary: &signer, amount: u64): option::Option> acquires MyPledges, BeneficiaryPolicy { - let address_of_beneficiary = signer::address_of(sig_beneficiary); - if (!exists(address_of_beneficiary)) { - return option::none>() - }; - - let pledgers = *&borrow_global(address_of_beneficiary).pledgers; - let amount_available = *&borrow_global(address_of_beneficiary).amount_available; - - if (amount_available == 0 || amount == 0) { - return option::none>() - }; - - let pct_withdraw = fixed_point64::create_from_rational((amount as u128), (amount_available as u128)); - - let i = 0; - let all_coins = option::none>(); - while (i < vector::length(&pledgers)) { - let pledge_account = *vector::borrow(&pledgers, i); - - // DANGER: this is a private function that changes balances. - if (!exists(pledge_account)) continue; + let address_of_beneficiary = signer::address_of(sig_beneficiary); + if (!exists(address_of_beneficiary)) { + return option::none>() + }; - let c = withdraw_pct_from_one_pledge_account(&address_of_beneficiary, &pledge_account, &pct_withdraw); - // GROSS: dealing with options in Move. - // TODO: find a better way. - if (option::is_none(&all_coins) && option::is_some(&c)) { + let pledgers = *&borrow_global(address_of_beneficiary).pledgers; + let amount_available = *&borrow_global(address_of_beneficiary).amount_available; - let coin = option::extract(&mut c); - option::fill(&mut all_coins, coin); - option::destroy_none(c); - } else if (option::is_some(&c)) { + if (amount_available == 0 || amount == 0) { + return option::none>() + }; - let temp = option::extract(&mut all_coins); - let coin = option::extract(&mut c); - libra_coin::merge(&mut temp, coin); - option::destroy_none(all_coins); - all_coins = option::some(temp); - option::destroy_none(c); - } else { - option::destroy_none(c); - }; + let pct_withdraw = fixed_point64::create_from_rational((amount as u128), (amount_available as u128)); - i = i + 1; + let i = 0; + let all_coins = option::none>(); + while (i < vector::length(&pledgers)) { + let pledge_account = *vector::borrow(&pledgers, i); + + // DANGER: this is a private function that changes balances. + if (!exists(pledge_account)) continue; + + let c = withdraw_pct_from_one_pledge_account(&address_of_beneficiary, &pledge_account, &pct_withdraw); + // GROSS: dealing with options in Move. + // TODO: find a better way. + if (option::is_none(&all_coins) && option::is_some(&c)) { + + let coin = option::extract(&mut c); + option::fill(&mut all_coins, coin); + option::destroy_none(c); + } else if (option::is_some(&c)) { + + let temp = option::extract(&mut all_coins); + let coin = option::extract(&mut c); + libra_coin::merge(&mut temp, coin); + option::destroy_none(all_coins); + all_coins = option::some(temp); + option::destroy_none(c); + } else { + option::destroy_none(c); }; + i = i + 1; + }; + all_coins } - fun get_user_pledges(account: &address): vector
acquires - MyPledges { + fun get_user_pledges(account: &address): vector
acquires MyPledges { let list = vector::empty
(); if (!exists(*account)) return list; let user_state = borrow_global(*account); diff --git a/framework/libra-framework/sources/ol_sources/proof_of_fee.move b/framework/libra-framework/sources/ol_sources/proof_of_fee.move index 1dcb3e87a..bfc57be74 100644 --- a/framework/libra-framework/sources/ol_sources/proof_of_fee.move +++ b/framework/libra-framework/sources/ol_sources/proof_of_fee.move @@ -4,26 +4,26 @@ ///////////////////////////////////////////////////////////////////////// // NOTE: this module replaces NodeWeight.move, which becomes redundant since // all validators have equal weight in consensus. -// TODO: the bubble sort functions were lifted directly from NodeWeight, needs checking. /////////////////////////////////////////////////////////////////////////// - module ol_framework::proof_of_fee { use std::error; use std::signer; use std::vector; + use std::math64; use std::fixed_point32; use diem_framework::validator_universe; - use ol_framework::jail; - use ol_framework::slow_wallet; - use ol_framework::vouch; use diem_framework::transaction_fee; - use ol_framework::epoch_helper; use diem_framework::account; use diem_framework::stake; use diem_framework::system_addresses; + use ol_framework::jail; + use ol_framework::slow_wallet; + use ol_framework::vouch; + use ol_framework::epoch_helper; use ol_framework::globals; - // use diem_std::debug::print; + use ol_framework::address_utils; + //use diem_std::debug::print; friend diem_framework::genesis; friend ol_framework::epoch_boundary; @@ -32,14 +32,25 @@ module ol_framework::proof_of_fee { #[test_only] friend ol_framework::mock; - /// The nominal reward for each validator in each epoch. const GENESIS_BASELINE_REWARD: u64 = 1000000; /// Number of vals needed before PoF becomes competitive for /// performant nodes as well - const VAL_BOOT_UP_THRESHOLD: u64 = 19; - + const VAL_BOOT_UP_THRESHOLD: u64 = 21; + /// This figure is experimental and a different percentage may be finalized + /// after some experience in the wild. Additionally it could be dynamic + /// based on another function or simply randomized within a range + /// (as originally proposed in this feature request) + const PCT_REDUCTION_FOR_COMPETITION: u64 = 10; // 10% + /// Upper bound threshold for bid percentages. + const BID_UPPER_BOUND: u64 = 0950; // 95% + /// Lower bound threshold for bid percentages. + const BID_LOWER_BOUND: u64 = 0500; // 50% + /// Short window period for recent bid trends. + const SHORT_WINDOW: u64 = 5; // 5 epochs + /// Long window period for extended bid trends. + const LONG_WINDOW: u64 = 10; // 10 epochs //////// ERRORS ///////// /// Not an active validator @@ -63,7 +74,6 @@ module ol_framework::proof_of_fee { /// not enough coin balance const ELOW_UNLOCKED_COIN_BALANCE: u64 = 17; - // A struct on the validators account which indicates their // latest bid (and epoch) struct ProofOfFeeAuction has key { @@ -132,55 +142,84 @@ module ol_framework::proof_of_fee { } } - /// Consolidates all the logic for the epoch boundary/ - /// Includes: getting the sorted bids, - /// filling the seats (determined by MusicalChairs), and getting a price. - /// and finally charging the validators for their bid (everyone pays the lowest) - /// for audit instrumentation returns: final set size, auction winners, all the bidders, (including not-qualified), and all qualified bidders. - /// we also return the auction entry price (clearing price) + /// Consolidates all the logic for the epoch boundary, including: + /// 1. Getting the sorted bidders, + /// 2. Calculate final validators set size (number of seats to fill), + /// 3. Filling the seats, + /// 4. Getting a price, + /// 5. Finally charging the validators for their bid (everyone pays the lowest) + /// For audit instrumentation returns: final set size, auction winners, all the bidders, (including not-qualified), and all qualified bidders. + /// We also return the auction entry price (clearing price) /// (final_set_size, auction_winners, all_bidders, only_qualified_bidders, actually_paid, entry_fee) public(friend) fun end_epoch( vm: &signer, outgoing_compliant_set: &vector
, - final_set_size: u64 + mc_set_size: u64 // musical chairs set size suggestion ): (vector
, vector
, vector
, u64) acquires ProofOfFeeAuction, ConsensusReward { - system_addresses::assert_ol(vm); - - let all_bidders = get_bidders(false); - let only_qualified_bidders = get_bidders(true); - // The set size as determined by musical chairs is a target size - // but the actual final size depends on how much can we expand the set - // without adding too many unproven nodes (which we don't know if they are prepared to validate, and risk halting th network). - - - // Boot up - // After an upgrade or incident the network may need to rebuild the - // validator set from a small base. - // we should increase the available seats starting from a base of - // compliant nodes. And make it competitive for the unknown nodes. - // Instead of increasing the seats by +1 the compliant vals we should - // increase by compliant + (1/2 compliant - 1) or another - // safe threshold. - // Another effect is that with PoF we might be dropping compliant nodes, - // in favor of unknown nodes with high bids. - // So in the case of a small validator set, we ignore the musical_chairs - // suggestion, and increase the seats offered, and guarantee seats to - // performant nodes. - let performant_len = vector::length(outgoing_compliant_set); - if ( - performant_len < VAL_BOOT_UP_THRESHOLD && - performant_len > 2 - ) { - final_set_size = performant_len + (performant_len/2 - 1) - }; + system_addresses::assert_ol(vm); + + let all_bidders = get_bidders(false); + let only_qualified_bidders = get_bidders(true); + + // Calculate the final set size considering the number of compliant validators, + // number of qualified bidders, and musical chairs set size suggestion + let final_set_size = calculate_final_set_size( + vector::length(outgoing_compliant_set), + vector::length(&only_qualified_bidders), + mc_set_size); + + // This is the core of the mechanism, the uniform price auction + // the winners of the auction will be the validator set. + // Other lists are created for audit purposes of the BoundaryStatus + let (auction_winners, entry_fee, _clearing_bid, _proven, _unproven) = fill_seats_and_get_price(vm, final_set_size, &only_qualified_bidders, outgoing_compliant_set); + + (auction_winners, all_bidders, only_qualified_bidders, entry_fee) + } + - // This is the core of the mechanism, the uniform price auction - // the winners of the auction will be the validator set. - // other lists are created for audit purposes of the BoundaryStatus - let (auction_winners, entry_fee, _clearing_bid, _proven, _unproven) = fill_seats_and_get_price(vm, final_set_size, &only_qualified_bidders, outgoing_compliant_set); + // The set size as determined by musical chairs is a target size + // but the actual final size depends on: + // a. how much can we expand the set without adding too many unproven nodes (which we don't know + // if they are prepared to validate, and risk halting the network). + // b. how many qualified bidders are there, and how many seats can we offer to ensure competition. + + // 1. Boot Up + // After an upgrade or incident the network may need to rebuild the validator set + // from a small base. We should increase the available seats starting + // from a base of compliant nodes. And make it competitive for the unknown nodes. + // Instead of increasing the seats by +1 the compliant vals we should + // increase by compliant + (1/2 compliant - 1) or another safe threshold. + // Another effect is that with PoF we might be dropping compliant nodes, + // in favor of unknown nodes with high bids. So in the case of a small validator set, + // we ignore the musical_chairs suggestion, and increase the seats offered, and guarantee seats to + // performant nodes. + // + // 2. Competitive Set + // If we have more qualified bidders than the threshold, we should limit the final set size + // to 90% of the qualified bidders to ensure that vals will compete for seats. + + fun calculate_final_set_size( + outgoing_compliant: u64, + qualified_bidders: u64, + mc_set_size: u64 // musical chairs set size suggestion + ): u64 { + // 1. Boot Up + if ( + outgoing_compliant < VAL_BOOT_UP_THRESHOLD && + outgoing_compliant > 2 + ) { + return math64::min(outgoing_compliant + (outgoing_compliant/2 - 1), VAL_BOOT_UP_THRESHOLD) + }; + // 2. Competitive Set + if (mc_set_size >= VAL_BOOT_UP_THRESHOLD && qualified_bidders >= VAL_BOOT_UP_THRESHOLD) { + let seats_to_remove = qualified_bidders * PCT_REDUCTION_FOR_COMPETITION / 100; + let max_qualified = qualified_bidders - seats_to_remove; + // do note increase beyond musical chairs suggestion and competitive set size + return math64::min(max_qualified, mc_set_size) + }; - (auction_winners, all_bidders, only_qualified_bidders, entry_fee) + mc_set_size } /// The fees are charged seperate from the auction and seating loop @@ -197,9 +236,6 @@ module ol_framework::proof_of_fee { } - - - //////// CONSENSUS CRITICAL //////// // Get the validator universe sorted by bid // By default this will return a FILTERED list of validators @@ -234,11 +270,8 @@ module ol_framework::proof_of_fee { let k = 0; while (k < length) { // TODO: Ensure that this address is an active validator - let cur_address = *vector::borrow
(eligible_validators, k); - let (bid, _expire) = current_bid(cur_address); - let (_, qualified) = audit_qualification(cur_address); if (remove_unqualified && !qualified) { k = k + 1; @@ -249,37 +282,16 @@ module ol_framework::proof_of_fee { k = k + 1; }; - // Sorting the accounts vector based on value (weights). - // Bubble sort algorithm - let len_filtered = vector::length
(&filtered_vals); - // if there's only one person (testing) - if (len_filtered < 2) return (filtered_vals, bids); - let i = 0; - while (i < len_filtered){ - let j = 0; - while(j < len_filtered-i-1){ - - let value_j = *(vector::borrow(&bids, j)); - - let value_jp1 = *(vector::borrow(&bids, j+1)); - if(value_j > value_jp1){ - - vector::swap(&mut bids, j, j+1); - - vector::swap
(&mut filtered_vals, j, j+1); - }; - j = j + 1; - - }; - i = i + 1; - - }; - + // Sorting the accounts vector based on their bids + address_utils::sort_by_values(&mut filtered_vals, &mut bids); // Reverse to have sorted order - high to low. vector::reverse(&mut filtered_vals); vector::reverse(&mut bids); + // Shuffle duplicates to garantee randomness/fairness + address_utils::shuffle_duplicates(&mut filtered_vals, &mut bids); + return (filtered_vals, bids) } @@ -300,7 +312,7 @@ module ol_framework::proof_of_fee { // Here we place the bidders into their seats. // The order of the bids will determine placement. // One important aspect of picking the next validator set: - // it should have 2/3rds of known good ("proven") validators + // It should have 2/3rds of known good ("proven") validators // from the previous epoch. Otherwise the unproven nodes, who // may not be ready for consensus, might be offline and cause a halt. // Validators can be inattentive and have bids that qualify, but their nodes @@ -314,7 +326,9 @@ module ol_framework::proof_of_fee { // This is a potential issue again with inattentive validators who // have have a high bid, but again they fail repeatedly to finalize an epoch // successfully. Their bids should not penalize validators who don't have - // a streak of jailed epochs. So of the 1/3 unproven nodes, we'll first seat the validators with Jail.consecutive_failure_to_rejoin < 2, and after that the remainder. + // a streak of jailed epochs. So of the 1/3 unproven nodes, + // we'll first seat the validators with Jail.consecutive_failure_to_rejoin < 2, + // and after that the remainder. // There's some code implemented which is not enabled in the current form. // Unsealed auctions are tricky. The Proof Of Fee @@ -324,14 +338,18 @@ module ol_framework::proof_of_fee { // As such per epoch the validator is only allowed to revise their bids / // down once. To do this in practice they need to retract a bid (sit out // the auction), and then place a new bid. - // A validator can always leave the auction, but if they rejoin a second time in the epoch, then they've committed a bid until the next epoch. - // So retracting should be done with care. The ergonomics are not great. + // A validator can always leave the auction, but if they rejoin a second time in the epoch, + // then they've committed a bid until the next epoch. + // So retracting should be done with care. The ergonomics are not great. // The preference would be not to have this constraint if on the margins // the ergonomics brings more bidders than attackers. // After more experience in the wild, the network may decide to // limit bid retracting. - // The Validator must qualify on a number of metrics: have funds in their Unlocked account to cover bid, have miniumum viable vouches, and not have been jailed in the previous round. + // The Validator must qualify on a number of metrics: + // 1. have funds in their Unlocked account to cover bid, + // 2. have miniumum viable vouches, + // 3. and not have been jailed in the previous round. /// Showtime. /// This is where we take all the bidders and seat them. @@ -339,11 +357,11 @@ module ol_framework::proof_of_fee { /// This function assumes we have already filtered out ineligible validators. /// but we will check again here. /// we return: - // a. the list of winning validators (the validator set) - // b. the entry fee paid - // c. the clearing bid (percentage paid) - // d. the list of proven nodes added, for audit and instrumentation - // e. the list of unproven, for audit and instrumentation + /// a. the list of winning validators (the validator set) + /// b. the entry fee paid + /// c. the clearing bid (percentage paid) + /// d. the list of proven nodes added, for audit and instrumentation + /// e. the list of unproven, for audit and instrumentation public(friend) fun fill_seats_and_get_price( vm: &signer, final_set_size: u64, @@ -376,12 +394,12 @@ module ol_framework::proof_of_fee { (i < vector::length(&sorted_vals_by_bid)) ) { let val = vector::borrow(&sorted_vals_by_bid, i); - if (!account::exists_at(*val)) { - i = i + 1; - continue - }; + if (!account::exists_at(*val)) { + i = i + 1; + continue + }; // check if a proven node - // NOTE: if the top bidders all all "proven" nodes, then there will + // NOTE: if the top bidders are all "proven" nodes, then there will // be no reason to add an unproven. Unproven nodes will only // be picked if they have bids higher than the bottom 1/3 bids of the proven nodes if (vector::contains(proven_nodes, val)) { @@ -417,7 +435,7 @@ module ol_framework::proof_of_fee { cr.clearing_bid = lowest_bid_pct; if (lowest_bid_pct > 0) { - cr.entry_fee = fixed_point32::multiply_u64(cr.nominal_reward, bid_as_fixedpoint(lowest_bid_pct)); + cr.entry_fee = cr.nominal_reward * lowest_bid_pct / 1000; if (cr.nominal_reward > cr.entry_fee) { cr.net_reward = cr.nominal_reward - cr.entry_fee; @@ -472,119 +490,88 @@ module ol_framework::proof_of_fee { fun bid_as_fixedpoint(bid_pct: u64): fixed_point32::FixedPoint32 { fixed_point32::create_from_rational(bid_pct, 1000) } - /// Adjust the reward at the end of the epoch - /// as described in the paper, the epoch reward needs to be adjustable - /// given that the implicit bond needs to be sufficient, eg 5-10x the reward. + + + /// Calculates the reward adjustment based on bid history and nominal reward. + /// @param median_history - The median history of bids. + /// @param nominal_reward - The current nominal reward. /// @return Tuple (bool, bool, u64) /// 0: did the thermostat run, /// 1: did it increment, or decrease, bool /// 2: how much - /// if the thermostat returns (false, false, 0), it means there was an error running - public(friend) fun reward_thermostat(vm: &signer): (bool, bool, u64) acquires ConsensusReward { - system_addresses::assert_ol(vm); - // check the bid history - // if there are 5 days above 95% adjust the reward up by 5% - // adjust by more if it has been 10 days then, 10% - // if there are 5 days below 50% adjust the reward down. - // adjust by more if it has been 10 days then 10% - - let bid_upper_bound = 0950; - let bid_lower_bound = 0500; - - let short_window: u64 = 5; - let long_window: u64 = 10; - - let cr = borrow_global_mut(@ol_framework); - - - let len = vector::length(&cr.median_history); - let i = 0; - + fun calculate_reward_adjustment( + median_history: &vector, + nominal_reward: u64 + ): (bool, bool, u64) { + let history_length = vector::length(median_history); + let index = 0; let epochs_above = 0; let epochs_below = 0; - while (i < 16 && i < len) { // max ten days, but may have less in history, filling set should truncate the history at 15 epochs. - let avg_bid = *vector::borrow(&cr.median_history, i); + while (index < 16 && index < history_length) { + let avg_bid = *vector::borrow(median_history, index); - if (avg_bid > bid_upper_bound) { + if (avg_bid > BID_UPPER_BOUND) { epochs_above = epochs_above + 1; - } else if (avg_bid < bid_lower_bound) { + } else if (avg_bid < BID_LOWER_BOUND) { epochs_below = epochs_below + 1; }; - i = i + 1; + index = index + 1; }; - - if (cr.nominal_reward > 0) { - - - // TODO: this is an initial implementation, we need to - // decide if we want more granularity in the reward adjustment - // Note: making this readable for now, but we can optimize later + if (nominal_reward > 0) { if (epochs_above > epochs_below) { - - // if (epochs_above > short_window) { - - // check for zeros. - // TODO: put a better safety check here - - // If the Validators are bidding near 100% that means - // the reward is very generous, i.e. their opportunity - // cost is met at small percentages. This means the - // implicit bond is very high on validators. E.g. - // at 1% median bid, the implicit bond is 100x the reward. - // We need to DECREASE the reward - - - if (epochs_above > long_window) { - - // decrease the reward by 10% - let less_ten_pct = (cr.nominal_reward / 10); - cr.nominal_reward = cr.nominal_reward - less_ten_pct; + if (epochs_above > LONG_WINDOW) { + let less_ten_pct = (nominal_reward / 10); return (true, false, less_ten_pct) - - } else if (epochs_above > short_window) { - // decrease the reward by 5% - let less_five_pct = (cr.nominal_reward / 20); - cr.nominal_reward = cr.nominal_reward - less_five_pct; - + } else if (epochs_above > SHORT_WINDOW) { + let less_five_pct = (nominal_reward / 20); return (true, false, less_five_pct) } + } else { + if (epochs_below > LONG_WINDOW) { + let increase_ten_pct = (nominal_reward / 10); + return (true, true, increase_ten_pct) + } else if (epochs_below > SHORT_WINDOW) { + let increase_five_pct = (nominal_reward / 20); + return (true, true, increase_five_pct) + } }; - // return early since we can't increase and decrease simultaneously - - - // if validators are bidding low percentages - // it means the nominal reward is not high enough. - // That is the validator's opportunity cost is not met within a - // range where the bond is meaningful. - // For example: if the bids for the epoch's reward is 50% of the value, that means the potential profit, is the same as the potential loss. - // At a 25% bid (potential loss), the profit is thus 75% of the value, which means the implicit bond is 25/75, or 1/3 of the bond, the risk favors the validator. This means among other things, that an attacker can pay for the cost of the attack with the profits. See paper, for more details. + return (true, false, 0) + }; - // we need to INCREASE the reward, so that the bond is more meaningful. + (false, false, 0) + } - if (epochs_below > long_window) { - // increase the reward by 10% - let increase_ten_pct = (cr.nominal_reward / 10); - cr.nominal_reward = cr.nominal_reward + increase_ten_pct; - return (true, true, increase_ten_pct) - } else if (epochs_below > short_window) { - + /// Adjust the reward at the end of the epoch + /// as described in the paper, the epoch reward needs to be adjustable + /// given that the implicit bond needs to be sufficient, eg 5-10x the reward. + /// @param vm - The signer. + /// @return Tuple (bool, bool, u64) + /// 0: did the thermostat run, + /// 1: did it increment, or decrease, bool + /// 2: how much + /// if the thermostat returns (false, false, 0), it means there was an error running + public(friend) fun reward_thermostat(vm: &signer): (bool, bool, u64) acquires ConsensusReward { + system_addresses::assert_ol(vm); + let cr = borrow_global_mut(@ol_framework); - // increase the reward by 5% - let increase_five_pct = (cr.nominal_reward / 20); - cr.nominal_reward = cr.nominal_reward + increase_five_pct; - return (true, true, increase_five_pct) - }; + let (did_run, did_increment, amount) = calculate_reward_adjustment( + &cr.median_history, + cr.nominal_reward + ); - // we ran the thermostat but no change was made. - return (true, false, 0) + if (did_run) { + if (did_increment) { + cr.nominal_reward = cr.nominal_reward + amount; + } else { + cr.nominal_reward = cr.nominal_reward - amount; + } }; - // nominal reward is zero, there's a problem - return (false, false, 0) + (did_run, did_increment, amount) } /// find the median bid to push to history @@ -623,8 +610,6 @@ module ol_framework::proof_of_fee { } //////////////// GETTERS //////////////// - // get the current bid for a validator - #[view] /// get the baseline reward from ConsensusReward @@ -634,6 +619,7 @@ module ol_framework::proof_of_fee { return (b.nominal_reward, b.entry_fee, b.clearing_bid, b.median_win_bid) } + // get the current bid for a validator // CONSENSUS CRITICAL // ALL EYES ON THIS // Proof of Fee returns the current bid of the validator during the auction for upcoming epoch seats. @@ -665,21 +651,41 @@ module ol_framework::proof_of_fee { return (false, 0) } + #[view] + /// Query the reward adjustment without altering the nominal reward. + /// @param vm - The signer. + /// @return Tuple (bool, bool, u64) + /// 0: did the thermostat run, + /// 1: did it increment, or decrease, bool + /// 2: how much + /// if the thermostat returns (false, false, 0), it means there was an error running + public fun query_reward_adjustment(): (bool, bool, u64) acquires ConsensusReward { + let cr = borrow_global(@ol_framework); + + let (did_run, did_increment, amount) = calculate_reward_adjustment( + &cr.median_history, + cr.nominal_reward + ); + + (did_run, did_increment, amount) + } + + // Get the top N validators by bid, this is FILTERED by default public(friend) fun top_n_accounts(account: &signer, n: u64, unfiltered: bool): vector
acquires ProofOfFeeAuction, ConsensusReward { - system_addresses::assert_vm(account); + system_addresses::assert_vm(account); - let eligible_validators = get_bidders(unfiltered); - let len = vector::length
(&eligible_validators); - if(len <= n) return eligible_validators; + let eligible_validators = get_bidders(unfiltered); + let len = vector::length
(&eligible_validators); + if(len <= n) return eligible_validators; - let diff = len - n; - while(diff > 0){ - vector::pop_back(&mut eligible_validators); - diff = diff - 1; - }; + let diff = len - n; + while(diff > 0){ + vector::pop_back(&mut eligible_validators); + diff = diff - 1; + }; - eligible_validators + eligible_validators } @@ -708,13 +714,11 @@ module ol_framework::proof_of_fee { /// Note that the validator will not be bidding on any future /// epochs if they retract their bid. The must set a new bid. fun retract_bid(account_sig: &signer) acquires ProofOfFeeAuction { - let acc = signer::address_of(account_sig); if (!exists(acc)) { init(account_sig); }; - let pof = borrow_global_mut(acc); let this_epoch = epoch_helper::get_current_epoch(); @@ -725,7 +729,6 @@ module ol_framework::proof_of_fee { // assert!(this_epoch > pof.last_epoch_retracted, error::ol_tx(EABOVE_RETRACT_LIMIT)); //////// LEAVE COMMENTED. Code for a potential upgrade. //////// - pof.epoch_expiration = 0; pof.bid = 0; pof.last_epoch_retracted = this_epoch; @@ -905,10 +908,8 @@ module ol_framework::proof_of_fee { assert!(reward == 105, 1003); assert!(clear_percent == 50, 1004); assert!(median_bid == 33, 1005); - } - // Scenario: The reward is too low during 5 days (short window). People are not bidding very high. #[test(vm = @ol_framework)] fun thermostat_increase_long(vm: signer) acquires ConsensusReward { @@ -930,7 +931,6 @@ module ol_framework::proof_of_fee { i = i + 1; }; - test_mock_reward( &vm, 100, @@ -953,10 +953,8 @@ module ol_framework::proof_of_fee { assert!(reward == 110, 1003); assert!(clear_percent == 50, 1004); assert!(median_bid == 33, 1005); - } - // Scenario: The reward is too high during 5 days (short window). People are bidding over 95% of the baseline fee. #[test(vm = @ol_framework)] fun thermostat_decrease_short(vm: signer) acquires ConsensusReward { @@ -977,7 +975,6 @@ module ol_framework::proof_of_fee { i = i + 1; }; - test_mock_reward( &vm, 100, @@ -1000,10 +997,9 @@ module ol_framework::proof_of_fee { assert!(reward == 95, 1000); assert!(clear_percent == 50, 1004); assert!(median_bid == 33, 1005); - } - // Scenario: The reward is too low during 5 days (short window). People are not bidding very high. + // Scenario: The reward is too low during 5 days (short window). People are not bidding very high. #[test(vm = @ol_framework)] fun thermostat_decrease_long(vm: signer) acquires ConsensusReward { use diem_framework::chain_id; @@ -1024,7 +1020,6 @@ module ol_framework::proof_of_fee { i = i + 1; }; - test_mock_reward( &vm, 100, @@ -1047,7 +1042,6 @@ module ol_framework::proof_of_fee { assert!(reward == 90, 1003); assert!(clear_percent == 50, 1004); assert!(median_bid == 33, 1005); - } // #[test(vm = @ol_framework)] @@ -1063,4 +1057,306 @@ module ol_framework::proof_of_fee { // validator_universe::is_in_universe(@0x123); // } + + + // Calculate Final Set Size tests + + #[test] + fun test_calculate_final_set_size_boot_up_happy_day() { + // Happy Day: test complete boot up with plenty qualified bidders over multiple epochs + // having validators always compliant + + // Epoch 1 + let outgoing_compliant = 0; + let qualified_bidders = 100; + let mc_set_size = 4; + let result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + assert!(result == 4, 7357023); + + // Epoch 2 + outgoing_compliant = 4; + mc_set_size = 5; + result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // 4 + (4/2 - 1) = 5 + assert!(result == 5, 7357024); + + // Epoch 3 + outgoing_compliant = 5; + mc_set_size = 6; + result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // 5 + (5/2 - 1) = 6 + assert!(result == 6, 7357025); + + // Epoch 4 + outgoing_compliant = 6; + mc_set_size = 7; + result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // 6 + (6/2 - 1) = 8 + assert!(result == 8, 7357026); + + // Epoch 5 + outgoing_compliant = 8; + mc_set_size = 9; + result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // 8 + (8/2 - 1) = 11 + assert!(result == 11, 7357027); + + // Epoch 6 + outgoing_compliant = 11; + mc_set_size = 12; + result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // 11 + (11/2 - 1) = 15 + assert!(result == 15, 7357028); + + // Epoch 6 + outgoing_compliant = 15; + mc_set_size = 16; + result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // 15 + (15/2 - 1) = 21 + // min (21, 21) + assert!(result == 21, 7357028); + + // Epoch 7 - Boot up ended + outgoing_compliant = 21; + mc_set_size = 22; + result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // min(22, 100*90%) = 22 + assert!(result == 22, 7357029); + } + + #[test] + fun test_calculate_final_set_size_boot_up_threshold() { + let qualified_bidders = 100; + + // Test boot up increases maximum set size to 21 + let outgoing_compliant = 20; + let mc_set_size = 20; + let result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + assert!(result == 21, 7357030); + + // Test set size stays at 21 + outgoing_compliant = 21; + mc_set_size = 21; + result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + assert!(result == 21, 7357030); + + // Test set size increases to 22 + outgoing_compliant = 22; + mc_set_size = 22; + result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // min (22, 100*90%) = 22 + assert!(result == 22, 7357030); + } + + #[test] + fun test_calculate_final_set_size_competitive_set_no_changes() { + let outgoing_compliant = 21; + let qualified_bidders = 40; + let mc_set_size = 21; + let result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + assert!(result == 21, 7357030); + } + + #[test] + fun test_calculate_final_set_size_competitive_set_increases() { + let outgoing_compliant = 30; + let qualified_bidders = 40; + let mc_set_size = 31; + let result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + assert!(result == 31, 7357030); + } + + #[test] + fun test_calculate_final_set_size_competitive_set_decreases() { + let outgoing_compliant = 50; + let qualified_bidders = 50; + let mc_set_size = 50; + let result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + assert!(result == 45, 7357030); + } + + #[test] + fun test_calculate_final_set_size_competitive_set_decreases_to_boot_up() { + let outgoing_compliant = 21; + let qualified_bidders = 21; + let mc_set_size = 21; + let result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // min(21, 21*90%) = 19 + assert!(result == 19, 7357030); + + let outgoing_compliant = 21; + let qualified_bidders = 20; + let mc_set_size = 21; + let result = calculate_final_set_size(outgoing_compliant, qualified_bidders, mc_set_size); + // mc value + assert!(result == 21, 7357030); + } + + // Tests for calculate_reward_adjustment + + #[test] + public fun cra_nominal_reward_zero() { + let median_history = vector::empty(); + let nominal_reward = 0; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == false, 7357001); + assert!(did_increment == false, 7357002); + assert!(amount == 0, 7357003); + } + + #[test] + public fun cra_empty_bid_history() { + let median_history = vector::empty(); + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357004); + assert!(did_increment == false, 7357005); + assert!(amount == 0, 7357006); + } + + #[test] + public fun cra_less_than_16_bids() { + // 10 entries all with value 600 + let median_history = vector[600, 600, 600, 600, 600, 600, 600, 600, 600, 600]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357007); + assert!(did_increment == false, 7357008); + assert!(amount == 0, 7357009); + } + + #[test] + public fun cra_exactly_16_bids() { + // 16 entries all with value 600 + let median_history = vector[600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357010); + assert!(did_increment == false, 7357011); + assert!(amount == 0, 7357012); + } + + #[test] + public fun cra_more_than_16_bids() { + // 20 entries all with value 600 + let median_history = vector[600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357013); + assert!(did_increment == false, 7357014); + assert!(amount == 0, 7357015); + } + + #[test] + public fun cra_all_bids_above_upper_bound_short_window() { + // 6 entries all with value 960 + let median_history = vector[960, 960, 960, 960, 960, 960]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357016); + assert!(did_increment == false, 7357017); + assert!(amount == nominal_reward / 20, 7357018); + } + + #[test] + public fun cra_all_bids_above_upper_bound_long_window() { + // 11 entries all with value 960 + let median_history = vector[960, 960, 960, 960, 960, 960, 960, 960, 960, 960, 960]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357019); + assert!(did_increment == false, 7357020); + assert!(amount == nominal_reward / 10, 7357021); + } + + #[test] + public fun cra_all_bids_below_lower_bound_short_window() { + // 6 entries all with value 400 + let median_history = vector[400, 400, 400, 400, 400, 400]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357022); + assert!(did_increment == true, 7357023); + assert!(amount == nominal_reward / 20, 7357024); + } + + #[test] + public fun cra_all_bids_below_lower_bound_long_window() { + // 11 entries all with value 400 + let median_history = vector[400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357025); + assert!(did_increment == true, 7357026); + assert!(amount == nominal_reward / 10, 7357027); + } + + #[test] + public fun cra_mixed_bids_with_majority_above() { + // 9 entries above upper bound and 7 entries below lower bound + let median_history = vector[960, 960, 960, 960, 960, 960, 960, 960, 960, 400, 400, 400, 400, 400, 400, 400]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357028); + assert!(did_increment == false, 7357029); + assert!(amount == nominal_reward / 20, 7357030); // Since the total entries are 16, it falls under the short window + } + + #[test] + public fun cra_mixed_bids_with_majority_below() { + // 9 entries below lower bound and 7 entries above upper bound + let median_history = vector[400, 400, 400, 400, 400, 400, 400, 400, 400, 960, 960, 960, 960, 960, 960, 960]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357031); + assert!(did_increment == true, 7357032); + assert!(amount == nominal_reward / 20, 7357033); // Since the total entries are 16, it falls under the short window + } + + #[test] + public fun cra_mixed_bids_without_clear_majority() { + // 4 entries below lower bound and 4 entries above upper bound + let median_history = vector[400, 400, 400, 400, 960, 960, 960, 960]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357034); + assert!(did_increment == false, 7357035); + assert!(amount == 0, 7357036); + } + + #[test] + public fun cra_majority_above_long_window() { + // 12 entries above upper bound and 4 entries below lower bound + let median_history = vector[960, 960, 960, 960, 960, 960, 960, 960, 960, 960, 960, 960, 400, 400, 400, 400]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357037); + assert!(did_increment == false, 7357038); + assert!(amount == nominal_reward / 10, 7357039); // Majority above and longer than long window + } + + #[test] + public fun cra_majority_above_short_window() { + // 7 entries above upper bound and 4 entries below lower bound + let median_history = vector[960, 960, 960, 960, 960, 960, 960, 400, 400, 400, 400]; + let nominal_reward = 1000; + + let (did_run, did_increment, amount) = calculate_reward_adjustment(&median_history, nominal_reward); + assert!(did_run == true, 7357040); + assert!(did_increment == false, 7357041); + assert!(amount == nominal_reward / 20, 7357042); // Majority above and longer than short window but not long window + } } diff --git a/framework/libra-framework/sources/ol_sources/rank.todo b/framework/libra-framework/sources/ol_sources/rank.todo index d6527dab2..d7e869004 100644 --- a/framework/libra-framework/sources/ol_sources/rank.todo +++ b/framework/libra-framework/sources/ol_sources/rank.todo @@ -82,7 +82,7 @@ module ol_framework::rank{ }; // Sorting the accounts vector based on value (weights). - // Bubble sort algorithm + // Bubble sort algorithm => use address_utils::sort_by_values let len_filtered = vector::length(&weights); if (len_filtered < 2) return weights; let i = 0; diff --git a/framework/libra-framework/sources/ol_sources/tests/boundary.test.move b/framework/libra-framework/sources/ol_sources/tests/boundary.test.move index d89036ca8..659f207d1 100644 --- a/framework/libra-framework/sources/ol_sources/tests/boundary.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/boundary.test.move @@ -4,6 +4,13 @@ module ol_framework::test_boundary { use std::vector; use std::features; use diem_std::bls12381; + use diem_framework::stake; + use diem_framework::timestamp; + use diem_framework::reconfiguration; + use diem_framework::diem_governance; + use diem_framework::transaction_fee; + use diem_framework::account; + use ol_framework::burn; use ol_framework::mock; use ol_framework::proof_of_fee; use ol_framework::jail; @@ -14,12 +21,8 @@ module ol_framework::test_boundary { use ol_framework::epoch_boundary; use ol_framework::block; use ol_framework::ol_account; - use diem_framework::stake; - use diem_framework::reconfiguration; - use diem_framework::timestamp; - use diem_framework::diem_governance; - // use diem_std::debug::print; + use diem_std::debug::print; const Alice: address = @0x1000a; const Bob: address = @0x1000b; @@ -46,7 +49,13 @@ module ol_framework::test_boundary { // We need to test e2e of the epoch boundary #[test(root = @ol_framework)] fun e2e_boundary_happy(root: signer) { - let _vals = common_test_setup(&root); + let vals = common_test_setup(&root); + + // check vals balance + vector::for_each(vals, |addr| { + let (_unlocked, total) = ol_account::balance(addr); + assert!(total == 500_000, 7357000); + }); mock::trigger_epoch(&root); @@ -56,17 +65,28 @@ module ol_framework::test_boundary { // all validators were compliant, should be +1 of the 10 vals assert!(epoch_boundary::get_seats_offered() == 11, 7357002); - // all vals had winning bids, but it was less than the seats on offer assert!(vector::length(&epoch_boundary::get_auction_winners()) == 10, 7357003); // all of the auction winners became the validators ulitmately assert!(vector::length(&epoch_boundary::get_actual_vals()) == 10, 7357004); + + // check vals rewards received and bid fees collected + // balance + reward - fee = 500_000 + 1_000_000 - 10_000 = 1_490_000 + vector::for_each(vals, |addr| { + let (_unlocked, total) = ol_account::balance(addr); + assert!(total == 1_499_000, 7357005); + }); + + // check subsidy for new rewards and fees collected + // fees collected = 10 * 1_000_000 + 10 * 1_000 = 10_010_000 + print(&transaction_fee::system_fees_collected()); + assert!(transaction_fee::system_fees_collected() == 10_010_000, 7357006); } #[test(root = @ol_framework, alice = @0x1000a, marlon_rando = @0x12345)] fun e2e_add_validator_happy(root: signer, alice: signer, marlon_rando: signer) { - let _vals = common_test_setup(&root); + let initial_vals = common_test_setup(&root); // generate credentials for validator registration ol_account::transfer(&alice, @0x12345, 200000); @@ -77,33 +97,85 @@ module ol_framework::test_boundary { // MARLON PAYS THE BID let vals = validator_universe::get_eligible_validators(); - assert!(vector::length(&vals) == 11, 7357000); + assert!(vector::length(&vals) == 11, 7357001); + mock::mock_bids(&vals); // MARLON HAS MANY FRIENDS vouch::test_set_buddies(@0x12345, vals); let (errs, _pass) = proof_of_fee::audit_qualification(@0x12345); - assert!(vector::length(&errs) == 0, 7357001); + assert!(vector::length(&errs) == 0, 7357002); + + // get initial vals balance + let balances = vector::map(initial_vals, |addr| { + let (_unlocked, total) = ol_account::balance(addr); + total + }); mock::trigger_epoch(&root); - assert!(epoch_boundary::get_reconfig_success(), 7357002); + assert!(epoch_boundary::get_reconfig_success(), 7357003); // all validators were compliant, should be +1 of the 10 vals - assert!(epoch_boundary::get_seats_offered() == 11, 7357003); - + assert!(epoch_boundary::get_seats_offered() == 11, 7357004); // NOTE: now MARLON is INCLUDED in this, and we filled all the seats on offer. // all vals had winning bids, but it was less than the seats on offer - assert!(vector::length(&epoch_boundary::get_auction_winners()) == 11, 7357003); + assert!(vector::length(&epoch_boundary::get_auction_winners()) == 11, 7357005); // all of the auction winners became the validators ulitmately - assert!(vector::length(&epoch_boundary::get_actual_vals()) == 11, 7357004); + assert!(vector::length(&epoch_boundary::get_actual_vals()) == 11, 7357006); + + // check initial vals rewards received and bid fees collected + // previous balance = current - reward + fee + let i = 0; + while (i < vector::length(&initial_vals)) { + let (_unlocked, current) = ol_account::balance(*vector::borrow(&initial_vals, i)); + let previous = *vector::borrow(&balances, i); + assert!(current == (previous + 1_000_000 - 1_000), 7357007); + i = i + 1; + }; + + // check Marlon's balance: 200_000 - 1_000 = 199_000 + let (_unlocked, marlon_balance) = ol_account::balance(@0x12345); + assert!(marlon_balance == 199_000, 7357008); + + // check subsidy for new rewards and fees collected + // fees collected = 11 * 1_000_000 + 11 * 1_000 = 11_011_000 + assert!(transaction_fee::system_fees_collected() == 11_011_000, 7357009); // another epoch and everyone is compliant as well mock::mock_all_vals_good_performance(&root); mock::trigger_epoch(&root); - assert!(epoch_boundary::get_seats_offered() == 12, 7357005); - assert!(vector::length(&epoch_boundary::get_actual_vals()) == 11, 7357006); + + assert!(epoch_boundary::get_seats_offered() == 12, 7357010); + assert!(vector::length(&epoch_boundary::get_actual_vals()) == 11, 7357011); + + // check initial vals rewards received and bid fees collected + // previous balance = current - 2*reward + 2*fee + let i = 0; + while (i < vector::length(&initial_vals)) { + let (_unlocked, current) = ol_account::balance(*vector::borrow(&initial_vals, i)); + let previous = *vector::borrow(&balances, i); + assert!(current == (previous + 2_000_000 - 2_000), 7357012); + i = i + 1; + }; + + // check Marlon's balance: 200_000 + 1_000_000 - 2_000 = 1_198_000 + let (_unlocked, marlon_balance) = ol_account::balance(@0x12345); + assert!(marlon_balance == 1_198_000, 7357013); + + // CHECK BURNT FEES + + // prepare clean epoch + mock::trigger_epoch(&root); + + let (before, _) = burn::get_lifetime_tracker(); + + mock::trigger_epoch(&root); + + // check that only the entry fee sum is being burnt + let (after,_) = burn::get_lifetime_tracker(); + assert!(after - before == 11_000_000, 7357014); // scale 1_000 } #[test(root = @ol_framework, alice = @0x1000a, marlon_rando = @0x12345)] @@ -140,16 +212,42 @@ module ol_framework::test_boundary { fun e2e_boundary_excludes_jail(root: signer) { let vals = common_test_setup(&root); let alice_addr = *vector::borrow(&vals, 0); - jail::jail(&root, alice_addr); - assert!(jail::is_jailed(alice_addr), 7357000); + + // mock vals performance + let i = 1; + while (i < vector::length(&vals)) { + let addr = *vector::borrow(&vals, i); + stake::mock_performance(&root, addr, 10, 0); + i = i + 1; + }; + + // make Alice val not compliant to end up in jail + stake::mock_performance(&root, alice_addr, 10, 10); + + // get Alice balance before epoch boundary + let (_unlocked, alice_before) = ol_account::balance(alice_addr); + + // new epoch mock::trigger_epoch(&root); + // check that Alice is jailed + assert!(jail::is_jailed(alice_addr), 7357001); + + // ensure Alice did not receive rewards + let (_unlocked, alice_after) = ol_account::balance(alice_addr); + assert!(alice_before == alice_after, 7357002); + + // check that validator set reduced by 1 let qualified_bidders = epoch_boundary::get_qualified_bidders(); - assert!(vector::length(&qualified_bidders) == (vector::length(&vals) - 1), 7357003); + assert!(vector::length(&qualified_bidders) == 9, 7357003); + + // check subsidy for new rewards and fees collected + // fees collected = 9 * 1_000_000 + 9 * 2_000 = 9_018_000 + assert!(transaction_fee::system_fees_collected() == 9_018_000, 7357004); // all vals had winning bids, but it was less than the seats on offer - assert!(vector::length(&epoch_boundary::get_auction_winners()) == vector::length(&qualified_bidders) , 7357003); - assert!(epoch_boundary::get_reconfig_success(), 7357001); + assert!(vector::length(&epoch_boundary::get_auction_winners()) == vector::length(&qualified_bidders) , 7357005); + assert!(epoch_boundary::get_reconfig_success(), 7357006); } #[test(root = @ol_framework, marlon = @0x12345)] @@ -178,6 +276,65 @@ module ol_framework::test_boundary { assert!(epoch == 4, 7357003); } + #[test(root = @ol_framework)] + fun epoch_increase_thermostat(root: &signer) { + let vals = common_test_setup(root); + + // mock bids + vector::for_each(vals, |a| { + let sig = account::create_signer_for_test(a); + proof_of_fee::pof_update_bid(&sig, 0100, 30); // 10% for 30 epochs + }); + + // mock bid history to increase thermostat by 5% + proof_of_fee::test_mock_reward( + root, + 1000, // nominal reward + 0100, // clearing bid + 0100, // median win bid + vector[ 0100, 0100, 0100, 0100, 0100, 0100 ] // median history bellow 50% + ); + + // trigger epoch + mock::trigger_epoch(root); + + // check subsidy increased by 5% + // fees collected = entry fee + reward * 105% + // entry fee = 100 * 10 = 1_000 + // reward = 1050 * 10 = 10_500 + // fees collected = 11_500 + assert!(transaction_fee::system_fees_collected() == 11_500, 7357001); + } + + #[test(root = @ol_framework)] + fun epoch_decrease_thermostat(root: &signer) { + let vals = common_test_setup(root); + + // mock bids + vector::for_each(vals, |a| { + let sig = account::create_signer_for_test(a); + proof_of_fee::pof_update_bid(&sig, 0970, 30); // 97% for 30 epochs + }); + + // mock bid history to decrease thermostat by 5% + proof_of_fee::test_mock_reward( + root, + 100_000, // nominal reward + 0970, // clearing bid + 0970, // median win bid + vector[ 0970, 0970, 0970, 0970, 0970, 0970 ] // median history above 95% + ); + + // trigger epoch + mock::trigger_epoch(root); + + // check subsidy decreased by 5% + // fees collected = entry fee + reward * 95% + // entry fee = 97_000 * 10 = 970_000 + // reward = 95_000 * 10 = 950_000 + // fees collected = 1_920_000 + assert!(transaction_fee::system_fees_collected() == 1_920_000, 7357001); + } #[test(root = @ol_framework, marlon = @0x12345)] #[expected_failure(abort_code = 2, location = 0x1::epoch_boundary)] diff --git a/framework/libra-framework/sources/ol_sources/tests/burn.test.move b/framework/libra-framework/sources/ol_sources/tests/burn.test.move index 2f79941ad..51c66a9f6 100644 --- a/framework/libra-framework/sources/ol_sources/tests/burn.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/burn.test.move @@ -66,303 +66,297 @@ module ol_framework::test_burn { } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000d, eve = @0x1000e)] - fun burn_to_match_index(root: &signer, alice: &signer, bob: &signer, eve: &signer) { - // Scenario: - // There are two community wallets. Alice and Bob's - // EVE donates to both. But mostly to Alice. - // The Match Index, should reflect that. - - let vals = mock::genesis_n_vals(root, 5); // need to include eve to init funds - mock::ol_initialize_coin_and_fund_vals(root, 1000000, true); - // start at epoch 1, since turnout tally needs epoch info, and 0 may cause issues - mock::trigger_epoch(root); - - let (communityA, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); - let addr_A = signer::address_of(&communityA); - community_wallet_init::init_community(&communityA, vals, 2); // make all the vals signers - - let (communityB, _cap) = ol_account::test_ol_create_resource_account(bob, b"0xdeadbeef"); - let addr_B = signer::address_of(&communityB); - community_wallet_init::init_community(&communityB, vals, 2); // make all the vals signers - - let eve_donation_to_A = 75; - ol_account::transfer(eve, addr_A, eve_donation_to_A); - let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_A); - assert!(total_funds_sent_by_eve == eve_donation_to_A, 7357001); - - let eve_donation_to_B = 25; - ol_account::transfer(eve, addr_B, eve_donation_to_B); - let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_B); - assert!(total_funds_sent_by_eve == eve_donation_to_B, 7357002); - - let qualifying = vector::singleton(addr_A); - vector::push_back(&mut qualifying, addr_B); - - // simulate index recalculation at epoch boundary - match_index::test_reset_ratio(root, qualifying); - let (_addrs_vec, _index_vec, ratio_vec) = match_index::get_ratios(); - - // so far addr_A should have 75 coins from Eve's transfer - let(_, balance_pre) = ol_account::balance(addr_A); - assert!(balance_pre == eve_donation_to_A, 7357003); - - // let's burn some of bob's coins - // first he sets his burn preference - burn::set_send_community(bob, true); - let bob_burn = 100000; - let c = ol_account::withdraw(bob, bob_burn); - // then the system burns based on his preference. - burn::test_vm_burn_with_user_preference(root, signer::address_of(bob), c); - // check that a recycle to match index happened instead of a straight burn - let (_lifetime_burn, lifetime_match) = burn::get_lifetime_tracker(); - assert!(lifetime_match > 0, 7357003); - - - // the balance of address A should be increase by the ratio above 75% * bob's burn of 100_000 - let ratio_A = vector::borrow(&ratio_vec, 0); - let bob_burn_share_A = fixed_point32::multiply_u64(bob_burn, *ratio_A); - - let(_, balance) = ol_account::balance(addr_A); - assert!(balance > balance_pre, 7357004); - assert!(balance == (bob_burn_share_A + eve_donation_to_A), 7357005); - } - - - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000d, eve = @0x1000e)] - fun simple_burn(root: &signer, alice: &signer, bob: &signer, eve: &signer) { - // Scenario: - // There are two community wallets. Alice and Bob's - // EVE donates to both. But mostly to Alice. - // The Match Index, should reflect that. - - let vals = mock::genesis_n_vals(root, 5); // need to include eve to init funds - mock::ol_initialize_coin_and_fund_vals(root, 1000000, true); - // start at epoch 1, since turnout tally needs epoch info, and 0 may cause issues - mock::trigger_epoch(root); - - let (communityA, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); - let addr_A = signer::address_of(&communityA); - community_wallet_init::init_community(&communityA, vals, 2); // make all the vals signers - - let (communityB, _cap) = ol_account::test_ol_create_resource_account(bob, b"0xdeadbeef"); - let addr_B = signer::address_of(&communityB); - community_wallet_init::init_community(&communityB, vals, 2); // make all the vals signers - - let eve_donation_to_A = 75; - ol_account::transfer(eve, addr_A, eve_donation_to_A); - let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_A); - assert!(total_funds_sent_by_eve == eve_donation_to_A, 7357001); - - let eve_donation_to_B = 25; - ol_account::transfer(eve, addr_B, eve_donation_to_B); - let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_B); - assert!(total_funds_sent_by_eve == eve_donation_to_B, 7357002); - - let qualifying = vector::singleton(addr_A); - vector::push_back(&mut qualifying, addr_B); - - // simulate index recalculation at epoch boundary - match_index::test_reset_ratio(root, qualifying); - // let (_addrs_vec, _index_vec, ratio_vec) = match_index::get_ratios(); - - // so far addr_A should have 75 coins from Eve's transfer - let(_, balance_pre) = ol_account::balance(addr_A); - assert!(balance_pre == eve_donation_to_A, 7357003); - - // let's burn some of bob's coins - // first he sets his burn preference - // IMPORTANT: BOB IS SETTING PREFERENCE TO SIMPLE BURN - burn::set_send_community(bob, false); - let bob_burn = 100000; - let c = ol_account::withdraw(bob, bob_burn); - // then the system burns based on his preference. - burn::test_vm_burn_with_user_preference(root, signer::address_of(bob), c); - - // no match recycling happened - let (lifetime_burn, lifetime_match) = burn::get_lifetime_tracker(); - assert!(lifetime_match == 0, 7357003); - assert!(lifetime_burn > 0, 7357004); - - - // NONE OF THE MATCH INDEX ACCOUNTS HAVE ANY CHANGE - let(_, balance) = ol_account::balance(addr_A); - assert!(balance == balance_pre, 7357005); - assert!(balance == eve_donation_to_A, 7357006); - } - - #[test(root = @ol_framework)] - fun epoch_fees_burn(root: &signer) { - // Scenario: - // the network starts. - // we mock some tx fees going into the collection - // the epoch turns and the fee account should be empty - // and the supply should be lower - - let n_vals = 5; - let _vals = mock::genesis_n_vals(root, n_vals); // need to include eve to init funds - let genesis_mint = 1000000; // 1 coin per - let epoch_reward = genesis_mint; // just to be explicit - - mock::ol_initialize_coin_and_fund_vals(root, genesis_mint, true); - let supply_pre = libra_coin::supply(); - let mocked_tx_fees = 1000000 * 100; // 100 coins in tx fee account - // 105 coins total - assert!(supply_pre == mocked_tx_fees + (n_vals * genesis_mint), 73570001); - - let fees = transaction_fee::system_fees_collected(); - assert!(fees == mocked_tx_fees, 73570002); - // start at epoch 1. NOTE Validators WILL GET PAID FOR EPOCH 1 - mock::trigger_epoch(root); // under 1000 rounds we don't - // evaluate performance of validators - let fees = transaction_fee::system_fees_collected(); - // There should be no fees left in tx fee wallet - assert!(fees == 0, 73570003); - - let validator_rewards = epoch_reward * n_vals; - - // of the initial supply, - // we expect to burn everything which was in the TX FEEs AFTER PAYING VALIDATOR REWARDS - let amount_burned_excess_tx_account = mocked_tx_fees - validator_rewards; - - // So the current supply should be lower, - // The the old supply was reduced by what was burned (the excess in tx bucket) - - let supply_post = libra_coin::supply(); - - assert!(supply_post == supply_pre - amount_burned_excess_tx_account, 73570003); - - // this ALSO means that the only supply left in this example - // is the rewards to validators from genesis, and from epoch 1 - assert!(supply_post == validator_rewards + (n_vals * genesis_mint), 73570003); - - } - - - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000d, eve = @0x1000e)] - fun epoch_fees_match(root: &signer, alice: &signer, bob: &signer, eve: &signer) { - // Scenario: - // There are two community wallets. Alice and Bob's - // EVE donates to both. But mostly to Alice. - // The Match Index, should reflect that. - - let vals = mock::genesis_n_vals(root, 5); // need to include eve to init funds - mock::ol_initialize_coin_and_fund_vals(root, 1000000, true); - // start at epoch 1, since turnout tally needs epoch info, and 0 may cause issues - mock::trigger_epoch(root); - - let (communityA, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); - let addr_A = signer::address_of(&communityA); - community_wallet_init::init_community(&communityA, vals, 2); // make all the vals signers - - let (communityB, _cap) = ol_account::test_ol_create_resource_account(bob, b"0xdeadbeef"); - let addr_B = signer::address_of(&communityB); - community_wallet_init::init_community(&communityB, vals, 2); // make all the vals signers - - let eve_donation_to_A = 75; - ol_account::transfer(eve, addr_A, eve_donation_to_A); - let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_A); - assert!(total_funds_sent_by_eve == eve_donation_to_A, 7357001); - - let eve_donation_to_B = 25; - ol_account::transfer(eve, addr_B, eve_donation_to_B); - let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_B); - assert!(total_funds_sent_by_eve == eve_donation_to_B, 7357002); - - let qualifying = vector::singleton(addr_A); - vector::push_back(&mut qualifying, addr_B); - - // simulate index recalculation at epoch boundary - match_index::test_reset_ratio(root, qualifying); - let (_addrs_vec, _index_vec, ratio_vec) = match_index::get_ratios(); - - // so far addr_A should have 75 coins from Eve's transfer - let(_, balance_pre) = ol_account::balance(addr_A); - assert!(balance_pre == eve_donation_to_A, 7357003); - - // let's burn some of bob's coins - // first he sets his burn preference - burn::set_send_community(bob, true); - let bob_burn = 100000; - let c = ol_account::withdraw(bob, bob_burn); - // then the system burns based on his preference. - burn::test_vm_burn_with_user_preference(root, signer::address_of(bob), c); - // check that a recycle to match index happened instead of a straight burn - let (_lifetime_burn, lifetime_match) = burn::get_lifetime_tracker(); - assert!(lifetime_match > 0, 7357003); + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000d, eve = @0x1000e)] + fun burn_to_match_index(root: &signer, alice: &signer, bob: &signer, eve: &signer) { + // Scenario: + // There are two community wallets. Alice and Bob's + // EVE donates to both. But mostly to Alice. + // The Match Index, should reflect that. + + let vals = mock::genesis_n_vals(root, 5); // need to include eve to init funds + mock::ol_initialize_coin_and_fund_vals(root, 1000000, true); + // start at epoch 1, since turnout tally needs epoch info, and 0 may cause issues + mock::trigger_epoch(root); + + let (communityA, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); + let addr_A = signer::address_of(&communityA); + community_wallet_init::init_community(&communityA, vals, 2); // make all the vals signers + + let (communityB, _cap) = ol_account::test_ol_create_resource_account(bob, b"0xdeadbeef"); + let addr_B = signer::address_of(&communityB); + community_wallet_init::init_community(&communityB, vals, 2); // make all the vals signers + + let eve_donation_to_A = 75; + ol_account::transfer(eve, addr_A, eve_donation_to_A); + let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_A); + assert!(total_funds_sent_by_eve == eve_donation_to_A, 7357001); + + let eve_donation_to_B = 25; + ol_account::transfer(eve, addr_B, eve_donation_to_B); + let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_B); + assert!(total_funds_sent_by_eve == eve_donation_to_B, 7357002); + + let qualifying = vector::singleton(addr_A); + vector::push_back(&mut qualifying, addr_B); + + // simulate index recalculation at epoch boundary + match_index::test_reset_ratio(root, qualifying); + let (_addrs_vec, _index_vec, ratio_vec) = match_index::get_ratios(); + + // so far addr_A should have 75 coins from Eve's transfer + let(_, balance_pre) = ol_account::balance(addr_A); + assert!(balance_pre == eve_donation_to_A, 7357003); + + // let's burn some of bob's coins + // first he sets his burn preference + burn::set_send_community(bob, true); + let bob_burn = 100000; + let c = ol_account::withdraw(bob, bob_burn); + // then the system burns based on his preference. + burn::test_vm_burn_with_user_preference(root, signer::address_of(bob), c); + // check that a recycle to match index happened instead of a straight burn + let (_lifetime_burn, lifetime_match) = burn::get_lifetime_tracker(); + assert!(lifetime_match > 0, 7357003); + + + // the balance of address A should be increase by the ratio above 75% * bob's burn of 100_000 + let ratio_A = vector::borrow(&ratio_vec, 0); + let bob_burn_share_A = fixed_point32::multiply_u64(bob_burn, *ratio_A); + + let(_, balance) = ol_account::balance(addr_A); + assert!(balance > balance_pre, 7357004); + assert!(balance == (bob_burn_share_A + eve_donation_to_A), 7357005); + } + + + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000d, eve = @0x1000e)] + fun simple_burn(root: &signer, alice: &signer, bob: &signer, eve: &signer) { + // Scenario: + // There are two community wallets. Alice and Bob's + // EVE donates to both. But mostly to Alice. + // The Match Index, should reflect that. + + let vals = mock::genesis_n_vals(root, 5); // need to include eve to init funds + mock::ol_initialize_coin_and_fund_vals(root, 1000000, true); + // start at epoch 1, since turnout tally needs epoch info, and 0 may cause issues + mock::trigger_epoch(root); + + let (communityA, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); + let addr_A = signer::address_of(&communityA); + community_wallet_init::init_community(&communityA, vals, 2); // make all the vals signers + + let (communityB, _cap) = ol_account::test_ol_create_resource_account(bob, b"0xdeadbeef"); + let addr_B = signer::address_of(&communityB); + community_wallet_init::init_community(&communityB, vals, 2); // make all the vals signers + + let eve_donation_to_A = 75; + ol_account::transfer(eve, addr_A, eve_donation_to_A); + let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_A); + assert!(total_funds_sent_by_eve == eve_donation_to_A, 7357001); + + let eve_donation_to_B = 25; + ol_account::transfer(eve, addr_B, eve_donation_to_B); + let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_B); + assert!(total_funds_sent_by_eve == eve_donation_to_B, 7357002); + + let qualifying = vector::singleton(addr_A); + vector::push_back(&mut qualifying, addr_B); + + // simulate index recalculation at epoch boundary + match_index::test_reset_ratio(root, qualifying); + // let (_addrs_vec, _index_vec, ratio_vec) = match_index::get_ratios(); + + // so far addr_A should have 75 coins from Eve's transfer + let(_, balance_pre) = ol_account::balance(addr_A); + assert!(balance_pre == eve_donation_to_A, 7357003); + + // let's burn some of bob's coins + // first he sets his burn preference + // IMPORTANT: BOB IS SETTING PREFERENCE TO SIMPLE BURN + burn::set_send_community(bob, false); + let bob_burn = 100000; + let c = ol_account::withdraw(bob, bob_burn); + // then the system burns based on his preference. + burn::test_vm_burn_with_user_preference(root, signer::address_of(bob), c); + + // no match recycling happened + let (lifetime_burn, lifetime_match) = burn::get_lifetime_tracker(); + assert!(lifetime_match == 0, 7357003); + assert!(lifetime_burn > 0, 7357004); + + + // NONE OF THE MATCH INDEX ACCOUNTS HAVE ANY CHANGE + let(_, balance) = ol_account::balance(addr_A); + assert!(balance == balance_pre, 7357005); + assert!(balance == eve_donation_to_A, 7357006); + } + + #[test(root = @ol_framework)] + fun epoch_fees_burn(root: &signer) { + // Scenario: + // the network starts. + // we mock some tx fees going into the collection + // the epoch turns and the fee account should be empty + // and the supply should be lower + let n_vals = 5; + let _vals = mock::genesis_n_vals(root, n_vals); // need to include eve to init funds + let genesis_mint = 1_000_000; // 1 coin per + let epoch_reward = genesis_mint; // just to be explicit - // the balance of address A should be increase by the ratio above 75% * bob's burn of 100_000 - let ratio_A = vector::borrow(&ratio_vec, 0); - let bob_burn_share_A = fixed_point32::multiply_u64(bob_burn, *ratio_A); - - let(_, balance) = ol_account::balance(addr_A); - assert!(balance > balance_pre, 7357004); - assert!(balance == (bob_burn_share_A + eve_donation_to_A), 7357005); - } + mock::ol_initialize_coin_and_fund_vals(root, genesis_mint, true); + let supply_pre = libra_coin::supply(); + let bruce_fortune = 100_000_000_000; + let mocked_tx_fees = 5_000_000 * 100; // 100 coins in tx fee account + assert!(supply_pre == mocked_tx_fees + bruce_fortune + (n_vals * genesis_mint), 73570001); + let fees = transaction_fee::system_fees_collected(); + assert!(fees == mocked_tx_fees, 73570002); - // TODO: + // start at epoch 1. NOTE Validators WILL GET PAID FOR EPOCH 1 + mock::trigger_epoch(root); // under 1000 rounds we don't - // #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000d, eve = @0x1000e)] - // fun fee_makers_calc(root: &signer, alice: &signer, bob: &signer, eve: &signer) { - // // Scenario: - // assert!(TransactionFee::get_fees_collected()==0, 735701); - // let coin = Diem::mint(&vm, 1); - // TransactionFee::pay_fee_and_track(@Alice, coin); + // evaluate performance of validators + let fees = transaction_fee::system_fees_collected(); + // There should be only entry fee left in tx fee wallet + assert!(fees == 5_000_000, 73570003); - // let fee_makers = TransactionFee::get_fee_makers(); - // assert!(Vector::length(&fee_makers)==1, 735702); - // assert!(TransactionFee::get_fees_collected()==1, 735703); - // } + let validator_rewards = epoch_reward * n_vals; - #[test(root=@ol_framework, alice=@0x1000a)] - fun track_fees(root: &signer, alice: address) { - // use ol_framework::libra_coin; - let _vals = mock::genesis_n_vals(root, 1); // need to include eve to init funds - mock::ol_initialize_coin_and_fund_vals(root, 10000, true); + // of the initial supply, + // we expect to burn everything which was in the TX FEEs AFTER PAYING VALIDATOR REWARDS + let amount_burned_excess_tx_account = mocked_tx_fees - validator_rewards; - let marlon_rando = @0x12345; - ol_account::create_account(root, marlon_rando); + // So the current supply should be lower, + // The the old supply was reduced by what was burned (the excess in tx bucket) + let supply_post = libra_coin::supply(); + assert!(supply_post == supply_pre - amount_burned_excess_tx_account, 73570003); - let rando_money = 5; - let coin_option = ol_account::test_vm_withdraw(root, alice, rando_money); + // this ALSO means that the only supply left in this example + // is the rewards to validators from genesis, and from epoch 1 + // and the bruce fortune used to pledge infra escrow + assert!(supply_post == validator_rewards + (n_vals * genesis_mint) + bruce_fortune, 73570003); + } - if (option::is_some(&coin_option)) { - let c = option::extract(&mut coin_option); - // shortcut: we have the VM use alices money to pay a fee for marlon. - transaction_fee::vm_pay_fee(root, marlon_rando, c); - }; - option::destroy_none(coin_option); + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000d, eve = @0x1000e)] + fun epoch_fees_match(root: &signer, alice: &signer, bob: &signer, eve: &signer) { + // Scenario: + // There are two community wallets. Alice and Bob's + // EVE donates to both. But mostly to Alice. + // The Match Index, should reflect that. + + let vals = mock::genesis_n_vals(root, 5); // need to include eve to init funds + mock::ol_initialize_coin_and_fund_vals(root, 1000000, true); + // start at epoch 1, since turnout tally needs epoch info, and 0 may cause issues + mock::trigger_epoch(root); + + let (communityA, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); + let addr_A = signer::address_of(&communityA); + community_wallet_init::init_community(&communityA, vals, 2); // make all the vals signers + + let (communityB, _cap) = ol_account::test_ol_create_resource_account(bob, b"0xdeadbeef"); + let addr_B = signer::address_of(&communityB); + community_wallet_init::init_community(&communityB, vals, 2); // make all the vals signers + + let eve_donation_to_A = 75; + ol_account::transfer(eve, addr_A, eve_donation_to_A); + let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_A); + assert!(total_funds_sent_by_eve == eve_donation_to_A, 7357001); + + let eve_donation_to_B = 25; + ol_account::transfer(eve, addr_B, eve_donation_to_B); + let (_, _, total_funds_sent_by_eve) = receipts::read_receipt(signer::address_of(eve), addr_B); + assert!(total_funds_sent_by_eve == eve_donation_to_B, 7357002); + + let qualifying = vector::singleton(addr_A); + vector::push_back(&mut qualifying, addr_B); + + // simulate index recalculation at epoch boundary + match_index::test_reset_ratio(root, qualifying); + let (_addrs_vec, _index_vec, ratio_vec) = match_index::get_ratios(); + + // so far addr_A should have 75 coins from Eve's transfer + let(_, balance_pre) = ol_account::balance(addr_A); + assert!(balance_pre == eve_donation_to_A, 7357003); + + // let's burn some of bob's coins + // first he sets his burn preference + burn::set_send_community(bob, true); + let bob_burn = 100000; + let c = ol_account::withdraw(bob, bob_burn); + // then the system burns based on his preference. + burn::test_vm_burn_with_user_preference(root, signer::address_of(bob), c); + // check that a recycle to match index happened instead of a straight burn + let (_lifetime_burn, lifetime_match) = burn::get_lifetime_tracker(); + assert!(lifetime_match > 0, 7357003); + + + // the balance of address A should be increase by the ratio above 75% * bob's burn of 100_000 + let ratio_A = vector::borrow(&ratio_vec, 0); + let bob_burn_share_A = fixed_point32::multiply_u64(bob_burn, *ratio_A); + + let(_, balance) = ol_account::balance(addr_A); + assert!(balance > balance_pre, 7357004); + assert!(balance == (bob_burn_share_A + eve_donation_to_A), 7357005); + } - let fees = transaction_fee::system_fees_collected(); - assert!(fees == 100000005, 735701); - // Fees will include the initialization by MOCK. ol_initialize_coin_and_fund_vals - - // marlon is the only fee maker (since genesis) - let fee_makers = fee_maker::get_fee_makers(); - // includes 0x1 which makes a deposit on - assert!(vector::length(&fee_makers)==1, 735702); - let marlon_fees_made = fee_maker::get_user_fees_made(marlon_rando); - assert!(marlon_fees_made == 5, 735703); + // TODO: - mock::trigger_epoch(root); - // should reset the fee counter on everything. - // TODO: no proof of fee was charged on alice. But this is changed in - // feature branch `coin-methods-hygiene` + // #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000d, eve = @0x1000e)] + // fun fee_makers_calc(root: &signer, alice: &signer, bob: &signer, eve: &signer) { + // // Scenario: + // assert!(TransactionFee::get_fees_collected()==0, 735701); + // let coin = Diem::mint(&vm, 1); + // TransactionFee::pay_fee_and_track(@Alice, coin); - let marlon_fees_made = fee_maker::get_user_fees_made(marlon_rando); - assert!(marlon_fees_made == 0, 735704); - let fee_makers = fee_maker::get_fee_makers(); - assert!(vector::length(&fee_makers)==0, 735705); - let fees = transaction_fee::system_fees_collected(); - assert!(fees == 0, 7357008); + // let fee_makers = TransactionFee::get_fee_makers(); + // assert!(Vector::length(&fee_makers)==1, 735702); + // assert!(TransactionFee::get_fees_collected()==1, 735703); + // } - } + #[test(root=@ol_framework, alice=@0x1000a)] + fun track_fees(root: &signer, alice: address) { + // use ol_framework::libra_coin; + let _vals = mock::genesis_n_vals(root, 1); // need to include eve to init funds + mock::ol_initialize_coin_and_fund_vals(root, 10000, true); + let marlon_rando = @0x12345; + ol_account::create_account(root, marlon_rando); + + let rando_money = 5; + let coin_option = ol_account::test_vm_withdraw(root, alice, rando_money); + + if (option::is_some(&coin_option)) { + let c = option::extract(&mut coin_option); + // shortcut: we have the VM use alices money to pay a fee for marlon. + transaction_fee::vm_pay_fee(root, marlon_rando, c); + }; + option::destroy_none(coin_option); + + let fees = transaction_fee::system_fees_collected(); + assert!(fees == 500_000_005, 7357001); + // Fees will include the initialization by MOCK. ol_initialize_coin_and_fund_vals + + // marlon is the only fee maker (since genesis) + let fee_makers = fee_maker::get_fee_makers(); + // includes 0x1 which makes a deposit on + assert!(vector::length(&fee_makers)==1, 7357002); + + let marlon_fees_made = fee_maker::get_user_fees_made(marlon_rando); + assert!(marlon_fees_made == 5, 7357003); + + mock::trigger_epoch(root); + + let marlon_fees_made = fee_maker::get_user_fees_made(marlon_rando); + assert!(marlon_fees_made == 0, 7357004); + let fee_makers = fee_maker::get_fee_makers(); + assert!(vector::length(&fee_makers) == 0, 7357005); + let fees = transaction_fee::system_fees_collected(); + assert!(fees == 1_000_000, 7357006); // val set entry fee + } #[test(root = @ol_framework, alice_val = @0x1000a)] fun test_init_burn_tracker(root: &signer, alice_val: &signer) { @@ -383,7 +377,6 @@ module ol_framework::test_burn { assert!(prev_balance == marlon_salary, 7357002); assert!(burn_at_last_calc == 0, 7357003); assert!(cumu_burn == 0, 7357004); - } #[test(root = @ol_framework, alice_val=@0x1000a)] diff --git a/framework/libra-framework/sources/ol_sources/tests/donor_voice.test.move b/framework/libra-framework/sources/ol_sources/tests/donor_voice.test.move index bff2ba214..2b39cd964 100644 --- a/framework/libra-framework/sources/ol_sources/tests/donor_voice.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/donor_voice.test.move @@ -40,7 +40,7 @@ module ol_framework::test_donor_voice { // vals claim the offer multi_action::claim_offer(alice, donor_voice_address); multi_action::claim_offer(bob, donor_voice_address); - + //need to be caged to finalize donor directed workflow and release control of the account multi_action::finalize_and_cage(&resource_sig, 2); @@ -61,7 +61,7 @@ module ol_framework::test_donor_voice { // vals claim the offer multi_action::claim_offer(alice, donor_voice_address); - multi_action::claim_offer(bob, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); //need to be caged to finalize donor directed workflow and release control of the account multi_action::finalize_and_cage(&resource_sig, vector::length(&vals)); @@ -578,7 +578,7 @@ module ol_framework::test_donor_voice { multi_action::claim_offer(alice, donor_voice_address); multi_action::claim_offer(bob, donor_voice_address); multi_action::claim_offer(carol, donor_voice_address); - + //need to be caged to finalize donor directed workflow and release control of the account multi_action::finalize_and_cage(&resource_sig, 2); @@ -676,7 +676,7 @@ module ol_framework::test_donor_voice { // Dave and Eve are unhappy, and vote to liquidate the account. let vals = mock::genesis_n_vals(root, 5); // need to include eve to init funds - mock::ol_initialize_coin_and_fund_vals(root, 100000, true); + mock::ol_initialize_coin_and_fund_vals(root, 100_000, true); // start at epoch 1, since turnout tally needs epoch info, and 0 may cause // issues mock::trigger_epoch(root); @@ -723,7 +723,6 @@ module ol_framework::test_donor_voice { assert!(*vector::borrow(&addrs, 0) == @0x1000e, 7357006); assert!(*vector::borrow(&addrs, 1) == @0x1000d, 7357007); - let eve_donation_pro_rata = vector::borrow(&refunds, 0); let superman_3 = 1; // rounding from fixed_point32 assert!((*eve_donation_pro_rata + superman_3) == eve_donation, 7357008); @@ -732,7 +731,6 @@ module ol_framework::test_donor_voice { let superman_3 = 1; // rounding from fixed_point32 assert!((*dave_donation_pro_rata + superman_3) == dave_donation, 7357009); - let (_, program_balance_pre) = ol_account::balance(donor_voice_address); let (_, eve_balance_pre) = ol_account::balance(@0x1000e); @@ -747,10 +745,9 @@ module ol_framework::test_donor_voice { // eve shoul have received funds back assert!(eve_balance > eve_balance_pre, 7357010); - let (lifetime_burn_now, _) = burn::get_lifetime_tracker(); // nothing should have been burned, it was a refund + let (lifetime_burn_now, _) = burn::get_lifetime_tracker(); assert!(lifetime_burn_now == lifetime_burn_pre, 7357011); - } #[test(root = @ol_framework, alice = @0x1000a, dave = @0x1000d, eve = @0x1000e)] diff --git a/framework/libra-framework/sources/ol_sources/tests/jail.test.move b/framework/libra-framework/sources/ol_sources/tests/jail.test.move index 4e0457d7e..8ea8cfbc1 100644 --- a/framework/libra-framework/sources/ol_sources/tests/jail.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/jail.test.move @@ -4,6 +4,7 @@ module ol_framework::test_jail { use std::vector; use ol_framework::mock; use ol_framework::jail; + use ol_framework::randomness; // use diem_std::debug::print; #[test(root = @ol_framework)] @@ -76,7 +77,7 @@ module ol_framework::test_jail { #[test(root = @ol_framework)] public entry fun sort_by_jail(root: signer) { - + randomness::initialize_for_testing(&root); let vals = mock::genesis_n_vals(&root, 5); let alice = *vector::borrow(&vals, 0); let bob = *vector::borrow(&vals, 1); diff --git a/framework/libra-framework/sources/ol_sources/tests/musical_chairs.test.move b/framework/libra-framework/sources/ol_sources/tests/musical_chairs.test.move index e6e6a485b..6305519be 100644 --- a/framework/libra-framework/sources/ol_sources/tests/musical_chairs.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/musical_chairs.test.move @@ -3,61 +3,59 @@ /// tests for external apis, and where a dependency cycle with genesis is created. module ol_framework::test_musical_chairs { - use ol_framework::musical_chairs; - use ol_framework::mock; - use std::vector; - use std::fixed_point32; + use ol_framework::musical_chairs; + use ol_framework::mock; + use std::vector; + use std::fixed_point32; - // use diem_std::debug::print; + // use diem_std::debug::print; - #[test(root = @ol_framework)] - public entry fun eval_compliance_happy(root: signer) { + #[test(root = @ol_framework)] + public entry fun eval_compliance_happy(root: signer) { - let musical_chairs_default_seats = 10; - let vals = mock::genesis_n_vals(&root, 5); - assert!(vector::length(&vals) == 5, 7357001); + let musical_chairs_default_seats = 10; + let vals = mock::genesis_n_vals(&root, 5); + assert!(vector::length(&vals) == 5, 7357001); - // all vals compliant - mock::mock_all_vals_good_performance(&root); - let epoch = 10; // we don't evaluate epoch 0, 1 - let round = 2000; // we don't evaluate is rounds are below 1000 (one thousand) - let (good, bad, ratio) = musical_chairs::test_eval_compliance(&root, vals, - epoch, round); - assert!(vector::length(&good) == 5, 7357002); - assert!(vector::length(&bad) == 0, 7357003); - assert!(fixed_point32::is_zero(ratio), 7357004); + // all vals compliant + mock::mock_all_vals_good_performance(&root); + let epoch = 10; // we don't evaluate epoch 0, 1 + let round = 2000; // we don't evaluate is rounds are below 1000 (one thousand) + let (good, bad, ratio) = musical_chairs::test_eval_compliance(&root, vals, + epoch, round); + assert!(vector::length(&good) == 5, 7357002); + assert!(vector::length(&bad) == 0, 7357003); + assert!(fixed_point32::is_zero(ratio), 7357004); + let (outgoing_compliant_set, new_set_size) = + musical_chairs::test_stop(&root, epoch, round); - let (outgoing_compliant_set, new_set_size) = - musical_chairs::test_stop(&root, epoch, round); + assert!(vector::length(&outgoing_compliant_set) == 5, 7357005); + assert!(new_set_size == (musical_chairs_default_seats + 1), 7357006); + } + #[test(root = @ol_framework)] + // only one seat opens up + public entry fun eval_compliance_one_val(root: signer) { - assert!(vector::length(&outgoing_compliant_set) == 5, 7357005); - assert!(new_set_size == (musical_chairs_default_seats + 1), 7357006); - } + let vals = mock::genesis_n_vals(&root, 5); + assert!(vector::length(&vals) == 5, 7357001); - #[test(root = @ol_framework)] - // only one seat opens up - public entry fun eval_compliance_one_val(root: signer) { + // all vals compliant + mock::mock_case_1(&root, *vector::borrow(&vals, 0)); - let vals = mock::genesis_n_vals(&root, 5); - assert!(vector::length(&vals) == 5, 7357001); + let epoch = 10; // we don't evaluate epoch 0, 1 + let round = 2000; // we don't evaluate is rounds are below 1000 (one thousand) + let (good, bad, bad_ratio) = musical_chairs::test_eval_compliance(&root, + vals, epoch, round); + assert!(vector::length(&good) == 1, 7357002); + assert!(vector::length(&bad) == 4, 7357003); + assert!(!fixed_point32::is_zero(bad_ratio), 7357004); + assert!(fixed_point32::create_from_rational(4, 5) == bad_ratio, 7357005); - // all vals compliant - mock::mock_case_1(&root, *vector::borrow(&vals, 0)); - let epoch = 10; // we don't evaluate epoch 0, 1 - let round = 2000; // we don't evaluate is rounds are below 1000 (one thousand) - let (good, bad, bad_ratio) = musical_chairs::test_eval_compliance(&root, - vals, epoch, round); - assert!(vector::length(&good) == 1, 7357002); - assert!(vector::length(&bad) == 4, 7357003); - assert!(!fixed_point32::is_zero(bad_ratio), 7357004); - assert!(fixed_point32::create_from_rational(4, 5) == bad_ratio, 7357005); + let (_outgoing_compliant_set, _new_set_size) = + musical_chairs::test_stop(&root, epoch, round); - - let (_outgoing_compliant_set, _new_set_size) = - musical_chairs::test_stop(&root, epoch, round); - - } + } } diff --git a/framework/libra-framework/sources/ol_sources/tests/proof_of_fee.test.move b/framework/libra-framework/sources/ol_sources/tests/proof_of_fee.test.move index 8be5ac065..26a849121 100644 --- a/framework/libra-framework/sources/ol_sources/tests/proof_of_fee.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/proof_of_fee.test.move @@ -21,8 +21,6 @@ module ol_framework::test_pof { const Eve: address = @0x1000e; const Frank: address = @0x1000f; - - #[test_only] fun mock_good_bid(_root: &signer, alice: &address) { let a_sig = account::create_signer_for_test(*alice); @@ -56,13 +54,11 @@ module ol_framework::test_pof { assert!(bid == 0, 1001); assert!(expires == 0, 1002); - proof_of_fee::pof_update_bid(&a_sig, 100, 0); let (bid, expires) = proof_of_fee::current_bid(*alice); assert!(bid == 100, 1003); assert!(expires == 0, 1004); - // now retract proof_of_fee::pof_retract_bid(a_sig); let (bid, expires) = proof_of_fee::current_bid(*alice); @@ -331,18 +327,15 @@ module ol_framework::test_pof { let sorted_two = proof_of_fee::get_bidders(true); assert!(vector::length(&sorted_two) != vector::length(&val_universe), 1004); - assert!(vector::length(&sorted_two) == vector::length(&val_universe) - 1, 1005); - } // We can send the fill seats function a list of validators, and the list of performing validators, and it will return the winning bidders and the bid. #[test(root = @ol_framework)] fun fill_seats_happy(root: signer) { let set = mock::genesis_n_vals(&root, 5); - mock::ol_initialize_coin_and_fund_vals(&root, 500000, true); let len = vector::length(&set); - + mock::ol_initialize_coin_and_fund_vals(&root, 500000, true); mock::pof_default(); slow_wallet::slow_wallet_epoch_drip(&root, 500000); @@ -351,7 +344,6 @@ module ol_framework::test_pof { assert!(vector::length(&sorted) == vector::length(&set), 1003); let (seats, _, _, _, _) = proof_of_fee::fill_seats_and_get_price(&root, len, &sorted, &sorted); - assert!(vector::contains(&seats, vector::borrow(&set, 0)), 1004); // filling the seat updated the computation of the consensu reward. @@ -359,7 +351,6 @@ module ol_framework::test_pof { assert!(reward == 1000000, 1005); assert!(clear_percent == 1, 1006); assert!(median_bid == 3, 1007); - } // We fill all the seats, and run the thermostat @@ -410,11 +401,9 @@ module ol_framework::test_pof { mock::ol_initialize_coin_and_fund_vals(&root, 500000, true); mock::pof_default(); - // Ok now EVE changes her mind. Will force the bid to expire. let a_sig = account::create_signer_for_test(*vector::borrow(&set, 4)); proof_of_fee::pof_update_bid(&a_sig, 0, 0); - slow_wallet::slow_wallet_epoch_drip(&root, 500000); let sorted = proof_of_fee::get_bidders(true); @@ -452,14 +441,9 @@ module ol_framework::test_pof { fun fill_seats_many_bidders(root: signer) { let set = mock::genesis_n_vals(&root, 5); mock::pof_default(); - mock::ol_initialize_coin_and_fund_vals(&root, 500000, true); - let sorted = proof_of_fee::get_bidders(true); - - - let set_size = 3; let (seats, _, _, _, _) = proof_of_fee::fill_seats_and_get_price(&root, set_size, &sorted, &sorted); @@ -610,4 +594,59 @@ module ol_framework::test_pof { assert!(median_bid == 3, 10014); } + // Tests for query_reward_adjustment + + #[test(root = @ol_framework)] + public fun test_query_reward_adjustment_no_change(root: &signer) { + use diem_framework::chain_id; + proof_of_fee::init_genesis_baseline_reward(root); + chain_id::initialize_for_test(root, 4); + + // 16 entries all with value 600 + let median_history = vector[600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600]; + let nominal_reward = 1000; + + proof_of_fee::test_mock_reward(root, nominal_reward, 500, 500, median_history); + + let (did_run, did_increment, amount) = proof_of_fee::query_reward_adjustment(); + assert!(did_run == true, 7357043); + assert!(did_increment == false, 7357044); + assert!(amount == 0, 7357045); + } + + #[test(root = @ol_framework)] + public fun test_query_reward_adjustment_increase(root: &signer) { + use diem_framework::chain_id; + proof_of_fee::init_genesis_baseline_reward(root); + chain_id::initialize_for_test(root, 4); + + // 11 entries all with value 400 + let median_history = vector[400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400]; + let nominal_reward = 1000; + + proof_of_fee::test_mock_reward(root, nominal_reward, 500, 500, median_history); + + let (did_run, did_increment, amount) = proof_of_fee::query_reward_adjustment(); + assert!(did_run == true, 7357046); + assert!(did_increment == true, 7357047); + assert!(amount == nominal_reward / 10, 7357048); + } + + #[test(root = @ol_framework)] + public fun test_query_reward_adjustment_decrease(root: &signer) { + use diem_framework::chain_id; + proof_of_fee::init_genesis_baseline_reward(root); + chain_id::initialize_for_test(root, 4); + + // 11 entries all with value 960 + let median_history = vector[960, 960, 960, 960, 960, 960, 960, 960, 960, 960, 960]; + let nominal_reward = 1000; + + proof_of_fee::test_mock_reward(root, nominal_reward, 500, 500, median_history); + + let (did_run, did_increment, amount) = proof_of_fee::query_reward_adjustment(); + assert!(did_run == true, 7357049); + assert!(did_increment == false, 7357050); + assert!(amount == nominal_reward / 10, 7357051); + } } diff --git a/framework/libra-framework/sources/ol_sources/tests/slow_wallet.test.move b/framework/libra-framework/sources/ol_sources/tests/slow_wallet.test.move index b640e0c26..e4b9103c6 100644 --- a/framework/libra-framework/sources/ol_sources/tests/slow_wallet.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/slow_wallet.test.move @@ -18,7 +18,7 @@ module ol_framework::test_slow_wallet { use std::vector; use std::signer; - // use diem_std::debug::print; + use diem_std::debug::print; #[test(root = @ol_framework)] // we are testing that genesis creates the needed struct @@ -112,8 +112,8 @@ module ol_framework::test_slow_wallet { fun test_epoch_drip(root: signer) { let set = mock::genesis_n_vals(&root, 4); mock::ol_initialize_coin_and_fund_vals(&root, 100, false); - let a = *vector::borrow(&set, 0); + let a = *vector::borrow(&set, 0); assert!(slow_wallet::is_slow(a), 7357000); assert!(slow_wallet::unlocked_amount(a) == 100, 735701); @@ -121,12 +121,13 @@ module ol_framework::test_slow_wallet { rewards::test_helper_pay_reward(&root, a, coin, 0); let (u, b) = ol_account::balance(a); - assert!(b==100000100, 735702); + print(&b); + assert!(b==500_000_100, 735702); assert!(u==100, 735703); slow_wallet::slow_wallet_epoch_drip(&root, 233); let (u, b) = ol_account::balance(a); - assert!(b==100000100, 735704); + assert!(b==500_000_100, 735704); assert!(u==333, 735705); } diff --git a/framework/libra-framework/sources/ol_sources/tests/stake.test.move b/framework/libra-framework/sources/ol_sources/tests/stake.test.move index bdb19a01c..047ec5ae8 100644 --- a/framework/libra-framework/sources/ol_sources/tests/stake.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/stake.test.move @@ -7,7 +7,7 @@ module ol_framework::test_stake { use ol_framework::testnet; use ol_framework::grade; - // use diem_std::debug::print; + //use diem_std::debug::print; // Scenario: can take 6 already initialized validators, from a previous set // and reduce the set to 3 of those validators. @@ -44,6 +44,9 @@ module ol_framework::test_stake { // within these ranges we have different rules to recover. #[test(root = @ol_framework)] fun failover_scenarios(root: signer) { + use diem_framework::randomness; + randomness::initialize_for_testing(&root); + let set = mock::genesis_n_vals(&root, 10); testnet::unset(&root); // set to production mode @@ -60,7 +63,7 @@ module ol_framework::test_stake { // should return 10 let proposed = vals; let cfg_list = stake::check_failover_rules(proposed, 11); - assert!(vector::length(&cfg_list) == 10, 1003); + assert!(vector::length(&cfg_list) == 10, 1002); // second case: // number of seats has no effect, as long as proposed in above mininum for // happy case(9). @@ -76,24 +79,24 @@ module ol_framework::test_stake { // First case: We qualified fewer vals (7) than our theshhold (10). The // number of seats (7) is same as qualifying (the typical case) let cfg_list = stake::check_failover_rules(proposed, 7); - assert!(vector::length(&cfg_list) == 7, 1004); + assert!(vector::length(&cfg_list) == 7, 1005); // Second case: we have more qualified vals (7) than we intended to seat (6) // but it's all below our minimum (9). Take everyone that qualified. // This case is likely a bug let cfg_list = stake::check_failover_rules(proposed, 6); - assert!(vector::length(&cfg_list) == 7, 1004); + assert!(vector::length(&cfg_list) == 7, 1006); // C) Defibrillator // First case: we are not qualifying enough validators to stay alive // let's check we can get at least up to the number of seats offered let cfg_list = stake::check_failover_rules(proposed, 8); - assert!(vector::length(&cfg_list) == 8, 1004); + assert!(vector::length(&cfg_list) == 8, 1007); // Second case : we are not qualifying enough validators to stay alive // but the seats proposed is unrealistic. Let's just get back to a healthy let cfg_list = stake::check_failover_rules(proposed, 50); - assert!(vector::length(&cfg_list) == 9, 1004); + assert!(vector::length(&cfg_list) == 9, 1008); // even an empty list, but with seats to fill will work let cfg_list = stake::check_failover_rules(vector::empty(), 8); @@ -103,23 +106,20 @@ module ol_framework::test_stake { // health (9), even if musical chairs picked more (12). (Though this case should // never happen) let cfg_list = stake::check_failover_rules(vector::empty(), 12); - assert!(vector::length(&cfg_list) == 9, 1004); + assert!(vector::length(&cfg_list) == 9, 1009); // D) Hail Mary. If nothing we've done gets us above 4 proposed validators, // then don't change anything. let cfg_list = stake::check_failover_rules(vector::empty(), 0); - assert!(vector::length(&cfg_list) == 4, 1004); + assert!(vector::length(&cfg_list) == 4, 1010); // Other corner cases // musical chairs fails, but we have a list of qualifying validators // above threshold let cfg_list = stake::check_failover_rules(vals, 2); - assert!(vector::length(&cfg_list) == 10, 1004); - - + assert!(vector::length(&cfg_list) == 10, 1011); } - // Scenario: in production mode, we can't have fewer than 4 validators. // when that happens, the first failover is to get the validators with the most successful proposals. #[test(root = @ol_framework)] diff --git a/framework/libra-framework/sources/ol_sources/tests/validator_reward.test.move b/framework/libra-framework/sources/ol_sources/tests/validator_reward.test.move index 8d41753b5..bcccaba78 100644 --- a/framework/libra-framework/sources/ol_sources/tests/validator_reward.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/validator_reward.test.move @@ -4,6 +4,7 @@ module ol_framework::test_reconfiguration { use std::vector; use diem_framework::stake; + use diem_framework::transaction_fee; // use diem_framework::coin; use ol_framework::mock; use ol_framework::testnet; @@ -12,90 +13,112 @@ module ol_framework::test_reconfiguration { use diem_framework::reconfiguration; use ol_framework::epoch_helper; use ol_framework::ol_account; + use ol_framework::infra_escrow; - // use diem_std::debug::print; + use diem_std::debug::print; // Scenario: all genesis validators make it to next epoch #[test(root = @ol_framework)] fun reconfig_reward_happy_case(root: signer) { - let vals = mock::genesis_n_vals(&root, 5); + let vals = mock::genesis_n_vals(&root, 5); + mock::pof_default(); + assert!(vector::length(&vals) == 5, 7357001); + let vals = stake::get_current_validators(); + assert!(vector::length(&vals) == 5, 7357002); + // all vals compliant + mock::mock_all_vals_good_performance(&root); + let (unlocked, alice_bal) = ol_account::balance(@0x1000a); + assert!(unlocked==0, 7367001); + assert!(alice_bal==0, 7357002); - mock::pof_default(); - assert!(vector::length(&vals) == 5, 7357001); - let vals = stake::get_current_validators(); - assert!(vector::length(&vals) == 5, 7357002); - // all vals compliant - mock::mock_all_vals_good_performance(&root); + let (reward_one, _entry_fee, _, _ ) = proof_of_fee::get_consensus_reward(); - let (unlocked, alice_bal) = ol_account::balance(@0x1000a); - assert!(unlocked==0, 7367001); - assert!(alice_bal==0, 7357002); + // The epoch's reward BEFORE reconfiguration + assert!(reward_one == 1000000, 7357004); - let (reward_one, _entry_fee, _, _ ) = proof_of_fee::get_consensus_reward(); - // The epoch's reward BEFORE reconfiguration - assert!(reward_one == 1000000, 7357004); - // run ol reconfiguration - mock::trigger_epoch(&root); + let infra = infra_escrow::infra_escrow_balance(); + print(&555); + print(&infra); - let vals = stake::get_current_validators(); + let subsidy = transaction_fee::system_fees_collected(); + print(&666); + print(&subsidy); - assert!(vector::length(&vals) == 5, 7357005); - // let alice_bal = libra_coin::balance(@0x1000a); - let (_unlocked, alice_bal) = ol_account::balance(@0x1000a); + // run ol reconfiguration + mock::trigger_epoch(&root); - let (_, entry_fee, _, _ ) = proof_of_fee::get_consensus_reward(); - // need to check that the user paid an PoF entry fee for next epoch. - // which means the balance will be the nominal reward, net of the PoF clearing price bid - assert!(alice_bal == (reward_one-entry_fee), 7357006) + let infra = infra_escrow::infra_escrow_balance(); + print(&5552); + print(&infra); + let subsidy = transaction_fee::system_fees_collected(); + print(&6662); + print(&subsidy); - } + let vals = stake::get_current_validators(); + assert!(vector::length(&vals) == 5, 7357005); + // let alice_bal = libra_coin::balance(@0x1000a); + let (_unlocked, alice_bal) = ol_account::balance(@0x1000a); - #[test(root = @ol_framework)] - fun drop_non_performing(root: signer) { - let _vals = mock::genesis_n_vals(&root, 5); - // mock::ol_initialize_coin(&root); - mock::pof_default(); - assert!(libra_coin::balance(@0x1000a) == 0, 7357000); + let (_, entry_fee, _, _ ) = proof_of_fee::get_consensus_reward(); + // need to check that the user paid an PoF entry fee for next epoch. + // which means the balance will be the nominal reward, net of the PoF clearing price bid + assert!(alice_bal == (reward_one - entry_fee), 7357006); - // NOTE: epoch 0 and 1 are a special case, we don't run performance grades on that one. Need to move two epochs ahead - reconfiguration::test_helper_increment_epoch_dont_reconfigure(1); - reconfiguration::test_helper_increment_epoch_dont_reconfigure(1); + // test new subsidy + let (reward_two, _entry_fee, _, _ ) = proof_of_fee::get_consensus_reward(); + print(&777); + print(&reward_two); + let new_budget = reward_two * 5; + print(&new_budget); - assert!(epoch_helper::get_current_epoch() == 2, 7357001); + let subsidy = transaction_fee::system_fees_collected(); + print(&888); + print(&subsidy); - let vals = stake::get_current_validators(); - assert!(vector::length(&vals) == 5, 7357002); + } - // all vals compliant - mock::mock_all_vals_good_performance(&root); + #[test(root = @ol_framework)] + fun drop_non_performing(root: signer) { + let _vals = mock::genesis_n_vals(&root, 5); + // mock::ol_initialize_coin(&root); + mock::pof_default(); + assert!(libra_coin::balance(@0x1000a) == 0, 7357000); - // make alice non performant - mock::mock_case_4(&root, *vector::borrow(&vals, 0)); + // NOTE: epoch 0 and 1 are a special case, we don't run performance grades on that one. Need to move two epochs ahead + reconfiguration::test_helper_increment_epoch_dont_reconfigure(1); + reconfiguration::test_helper_increment_epoch_dont_reconfigure(1); - let (reward, _, _, _ ) = proof_of_fee::get_consensus_reward(); + assert!(epoch_helper::get_current_epoch() == 2, 7357001); - // run ol reconfiguration - mock::trigger_epoch(&root); - // mock::trigger_epoch(&root); + let vals = stake::get_current_validators(); + assert!(vector::length(&vals) == 5, 7357002); - let vals = stake::get_current_validators(); + // all vals compliant + mock::mock_all_vals_good_performance(&root); + + // make alice non performant + mock::mock_case_4(&root, *vector::borrow(&vals, 0)); - // one validator missing. - assert!(vector::length(&vals) == 4, 7357003); - assert!(!vector::contains(&vals, &@0x1000a), 7357004); + let (reward, _, _, _ ) = proof_of_fee::get_consensus_reward(); - let (_, entry_fee, _, _ ) = proof_of_fee::get_consensus_reward(); + // run ol reconfiguration + mock::trigger_epoch(&root); - // alice doesn't get paid + let vals = stake::get_current_validators(); - assert!(libra_coin::balance(@0x1000a) == 0, 7357005); - // bob does - assert!(libra_coin::balance(@0x1000b) == (reward - entry_fee), 7357006); + // one validator missing. + assert!(vector::length(&vals) == 4, 7357003); + assert!(!vector::contains(&vals, &@0x1000a), 7357004); + let (_, entry_fee, _, _ ) = proof_of_fee::get_consensus_reward(); + // alice doesn't get paid + assert!(libra_coin::balance(@0x1000a) == 0, 7357005); + // bob does + assert!(libra_coin::balance(@0x1000b) == (reward - entry_fee), 7357006); } diff --git a/framework/libra-framework/sources/randomness.move b/framework/libra-framework/sources/randomness.move index 53061cce5..7fab83a78 100644 --- a/framework/libra-framework/sources/randomness.move +++ b/framework/libra-framework/sources/randomness.move @@ -342,7 +342,7 @@ module diem_framework::randomness { /// Generate a permutation of `[0, 1, ..., n-1]` uniformly at random. /// If n is 0, returns the empty vector. - fun permutation(n: u64): vector acquires PerBlockRandomness { + public fun permutation(n: u64): vector acquires PerBlockRandomness { let values = vector[]; if(n == 0) { diff --git a/framework/libra-framework/sources/stake.move b/framework/libra-framework/sources/stake.move index 4d1c77dfd..d98825ebf 100644 --- a/framework/libra-framework/sources/stake.move +++ b/framework/libra-framework/sources/stake.move @@ -15,6 +15,7 @@ module diem_framework::stake { use diem_framework::system_addresses; use ol_framework::slow_wallet; use ol_framework::testnet; + use ol_framework::address_utils; // use diem_std::debug::print; @@ -878,7 +879,7 @@ module diem_framework::stake { current_vals } - /// Bubble sort the validators by their proposal counts. + /// Sort the validators by their proposal counts. public fun get_sorted_vals_by_net_props(): vector
acquires ValidatorSet, ValidatorConfig, ValidatorPerformance { let eligible_validators = get_current_validators(); let length = vector::length
(&eligible_validators); @@ -905,36 +906,12 @@ module diem_framework::stake { }; // Sorting the accounts vector based on value (weights). - // Bubble sort algorithm - let len_filtered = vector::length
(&filtered_vals); - if (len_filtered < 2) return filtered_vals; - let i = 0; - while (i < len_filtered){ - let j = 0; - while(j < len_filtered-i-1){ - - let value_j = *(vector::borrow(&weights, j)); - - let value_jp1 = *(vector::borrow(&weights, j+1)); - if(value_j > value_jp1){ - - vector::swap(&mut weights, j, j+1); - - vector::swap
(&mut filtered_vals, j, j+1); - }; - j = j + 1; - - }; - i = i + 1; - - }; - + address_utils::sort_by_values(&mut filtered_vals, &mut weights); // Reverse to have sorted order - high to low. vector::reverse
(&mut filtered_vals); return filtered_vals - } //////// 0L //////// diff --git a/framework/releases/head.mrb b/framework/releases/head.mrb index b63243772..14d5715b6 100644 Binary files a/framework/releases/head.mrb and b/framework/releases/head.mrb differ diff --git a/tools/config/src/make_yaml_public_fullnode.rs b/tools/config/src/make_yaml_public_fullnode.rs index 04ee7e1e3..0ecd48389 100644 --- a/tools/config/src/make_yaml_public_fullnode.rs +++ b/tools/config/src/make_yaml_public_fullnode.rs @@ -191,6 +191,7 @@ pub async fn download_genesis(home_dir: Option) -> anyhow::Result<()> { .await? .json::>() .await?; + // Find the latest version by parsing version numbers and sorting let latest_version = resp .iter() @@ -202,12 +203,18 @@ pub async fn download_genesis(home_dir: Option) -> anyhow::Result<()> { "https://raw.githubusercontent.com/0LNetworkCommunity/epoch-archive-mainnet/main/upgrades", latest_version.unwrap_or(DEFAULT_WAYPOINT_VERSION) ); + // Fetch the latest waypoint let blob_bytes = reqwest::get(&latest_path).await?.bytes().await?; let home = home_dir.unwrap_or_else(libra_types::global_config_dir); let genesis_dir = home.join("genesis/"); + + // Ensure the genesis directory exists + std::fs::create_dir_all(&genesis_dir)?; + let p = genesis_dir.join("genesis.blob"); + // Write the genesis blob to the file std::fs::write(p, &blob_bytes)?; Ok(()) } @@ -289,11 +296,28 @@ async fn persist_genesis() { let path = p.path().to_owned(); - download_genesis(Some(path)).await.unwrap(); - let l = std::fs::read_dir(p.path()) - .unwrap() - .next() - .unwrap() - .unwrap(); - assert!(l.file_name().to_str().unwrap().contains("genesis.blob")); + // Ensure the directory exists + assert!( + std::fs::metadata(&path).is_ok(), + "Directory does not exist: {:?}", + path + ); + + // Verify path is a directory + assert!( + Path::new(&path).is_dir(), + "Path is not a directory: {:?}", + path + ); + + // Attempt to download genesis + download_genesis(Some(path.clone())).await.unwrap(); + + // Check if the genesis.blob file exists + let genesis_blob_path = path.join("genesis").join("genesis.blob"); + assert!( + genesis_blob_path.exists(), + "genesis.blob file does not exist: {:?}", + genesis_blob_path + ); }