diff --git a/execution_engine/src/runtime/auction_internal.rs b/execution_engine/src/runtime/auction_internal.rs index e2f0141eb1..afdc950625 100644 --- a/execution_engine/src/runtime/auction_internal.rs +++ b/execution_engine/src/runtime/auction_internal.rs @@ -189,6 +189,60 @@ where Ok(keys.len()) } + fn reservation_count(&mut self, bid_addr: &BidAddr) -> Result { + let reservation_prefix = bid_addr.reservation_prefix()?; + let reservation_keys = self + .context + .get_keys_with_prefix(&reservation_prefix) + .map_err(|exec_error| { + error!("RuntimeProvider::reservation_count {:?}", exec_error); + >::from(exec_error).unwrap_or(Error::Storage) + })?; + Ok(reservation_keys.len()) + } + + fn used_reservation_count(&mut self, bid_addr: &BidAddr) -> Result { + let delegator_prefix = bid_addr.delegators_prefix()?; + let delegator_keys = self + .context + .get_keys_with_prefix(&delegator_prefix) + .map_err(|exec_error| { + error!("RuntimeProvider::used_reservation_count {:?}", exec_error); + >::from(exec_error).unwrap_or(Error::Storage) + })?; + let delegator_account_hashes: Vec = delegator_keys + .into_iter() + .filter_map(|key| { + if let Key::BidAddr(BidAddr::Delegator { delegator, .. }) = key { + Some(delegator) + } else { + None + } + }) + .collect(); + + let reservation_prefix = bid_addr.reservation_prefix()?; + let reservation_keys = self + .context + .get_keys_with_prefix(&reservation_prefix) + .map_err(|exec_error| { + error!("RuntimeProvider::reservation_count {:?}", exec_error); + >::from(exec_error).unwrap_or(Error::Storage) + })?; + + let used_reservations_count = reservation_keys + .iter() + .filter(|reservation| { + if let Key::BidAddr(BidAddr::Reservation { delegator, .. }) = reservation { + delegator_account_hashes.contains(delegator) + } else { + false + } + }) + .count(); + Ok(used_reservations_count) + } + fn vesting_schedule_period_millis(&self) -> u64 { self.context .engine_config() diff --git a/execution_engine/src/runtime/mod.rs b/execution_engine/src/runtime/mod.rs index 0e5a2364c7..7ee865fda5 100644 --- a/execution_engine/src/runtime/mod.rs +++ b/execution_engine/src/runtime/mod.rs @@ -1010,6 +1010,9 @@ where Self::try_get_named_argument(runtime_args, auction::ARG_RESERVED_SLOTS)? .unwrap_or(0); + let max_delegators_per_validator = + self.context.engine_config().max_delegators_per_validator(); + let result = runtime .add_bid( account_hash, @@ -1018,6 +1021,7 @@ where minimum_delegation_amount, maximum_delegation_amount, reserved_slots, + max_delegators_per_validator, ) .map_err(Self::reverter)?; @@ -1161,6 +1165,32 @@ where CLValue::from_t(()).map_err(Self::reverter) })(), + auction::METHOD_ADD_RESERVATIONS => (|| { + runtime.charge_system_contract_call(auction_costs.add_reservations)?; + + let reservations = + Self::get_named_argument(runtime_args, auction::ARG_RESERVATIONS)?; + + runtime + .add_reservations(reservations) + .map_err(Self::reverter)?; + + CLValue::from_t(()).map_err(Self::reverter) + })(), + auction::METHOD_CANCEL_RESERVATIONS => (|| { + runtime.charge_system_contract_call(auction_costs.cancel_reservations)?; + + let validator = Self::get_named_argument(runtime_args, auction::ARG_VALIDATOR)?; + let delegators = Self::get_named_argument(runtime_args, auction::ARG_DELEGATORS)?; + let max_delegators_per_validator = + self.context.engine_config().max_delegators_per_validator(); + + runtime + .cancel_reservations(validator, delegators, max_delegators_per_validator) + .map_err(Self::reverter)?; + + CLValue::from_t(()).map_err(Self::reverter) + })(), _ => CLValue::from_t(()).map_err(Self::reverter), }; diff --git a/execution_engine_testing/tests/src/test/regression/gov_89_regression.rs b/execution_engine_testing/tests/src/test/regression/gov_89_regression.rs index 9413febe1b..1bb327acf5 100644 --- a/execution_engine_testing/tests/src/test/regression/gov_89_regression.rs +++ b/execution_engine_testing/tests/src/test/regression/gov_89_regression.rs @@ -14,7 +14,8 @@ use casper_storage::data_access_layer::{SlashItem, StepResult}; use casper_types::{ execution::TransformKindV2, system::auction::{ - BidsExt, DelegationRate, SeigniorageRecipientsSnapshot, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, + BidsExt, DelegationRate, SeigniorageRecipientsSnapshotV2, + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, }, CLValue, EntityAddr, EraId, GenesisAccount, GenesisValidator, Key, Motes, ProtocolVersion, PublicKey, SecretKey, StoredValue, U512, @@ -86,7 +87,7 @@ fn should_not_create_any_purse() { .with_era_end_timestamp_millis(eras_end_timestamp_millis_1.as_millis().try_into().unwrap()) .build(); - let before_auction_seigniorage: SeigniorageRecipientsSnapshot = builder.get_value( + let before_auction_seigniorage: SeigniorageRecipientsSnapshotV2 = builder.get_value( EntityAddr::System(auction_hash.value()), SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, ); @@ -137,7 +138,7 @@ fn should_not_create_any_purse() { ); // seigniorage snapshot should have changed after auction - let after_auction_seigniorage: SeigniorageRecipientsSnapshot = builder.get_value( + let after_auction_seigniorage: SeigniorageRecipientsSnapshotV2 = builder.get_value( EntityAddr::System(auction_hash.value()), SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, ); diff --git a/execution_engine_testing/tests/src/test/step.rs b/execution_engine_testing/tests/src/test/step.rs index 95f360ac6e..66deb59a93 100644 --- a/execution_engine_testing/tests/src/test/step.rs +++ b/execution_engine_testing/tests/src/test/step.rs @@ -8,7 +8,7 @@ use casper_storage::data_access_layer::SlashItem; use casper_types::{ system::{ auction::{ - BidsExt, DelegationRate, SeigniorageRecipientsSnapshot, + BidsExt, DelegationRate, SeigniorageRecipientsSnapshotV2, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, }, mint::TOTAL_SUPPLY_KEY, @@ -82,7 +82,7 @@ fn should_step() { let auction_hash = builder.get_auction_contract_hash(); - let before_auction_seigniorage: SeigniorageRecipientsSnapshot = builder.get_value( + let before_auction_seigniorage: SeigniorageRecipientsSnapshotV2 = builder.get_value( EntityAddr::System(auction_hash.value()), SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, ); @@ -108,7 +108,7 @@ fn should_step() { ); // seigniorage snapshot should have changed after auction - let after_auction_seigniorage: SeigniorageRecipientsSnapshot = builder.get_value( + let after_auction_seigniorage: SeigniorageRecipientsSnapshotV2 = builder.get_value( EntityAddr::System(auction_hash.value()), SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, ); diff --git a/execution_engine_testing/tests/src/test/system_contracts/auction/distribute.rs b/execution_engine_testing/tests/src/test/system_contracts/auction/distribute.rs index 3a2fc46dbe..73042d00a0 100644 --- a/execution_engine_testing/tests/src/test/system_contracts/auction/distribute.rs +++ b/execution_engine_testing/tests/src/test/system_contracts/auction/distribute.rs @@ -4,6 +4,9 @@ use num_rational::Ratio; use num_traits::{CheckedMul, CheckedSub}; use once_cell::sync::Lazy; +use crate::test::system_contracts::auction::{ + get_delegator_staked_amount, get_era_info, get_validator_bid, +}; use casper_engine_test_support::{ ExecuteRequestBuilder, LmdbWasmTestBuilder, StepRequestBuilder, UpgradeRequestBuilder, DEFAULT_ACCOUNT_ADDR, DEFAULT_GENESIS_TIMESTAMP_MILLIS, DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS, @@ -17,12 +20,12 @@ use casper_types::{ account::AccountHash, runtime_args, system::auction::{ - self, BidsExt as _, DelegationRate, Delegator, EraInfo, SeigniorageAllocation, - SeigniorageRecipientsSnapshot, ValidatorBid, ARG_AMOUNT, ARG_DELEGATION_RATE, - ARG_DELEGATOR, ARG_PUBLIC_KEY, ARG_REWARDS_MAP, ARG_VALIDATOR, DELEGATION_RATE_DENOMINATOR, + self, BidsExt as _, DelegationRate, Delegator, SeigniorageAllocation, + SeigniorageRecipientsSnapshotV2, ARG_AMOUNT, ARG_DELEGATION_RATE, ARG_DELEGATOR, + ARG_PUBLIC_KEY, ARG_REWARDS_MAP, ARG_VALIDATOR, DELEGATION_RATE_DENOMINATOR, METHOD_DISTRIBUTE, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, }, - EntityAddr, EraId, Key, ProtocolVersion, PublicKey, SecretKey, Timestamp, U512, + EntityAddr, EraId, ProtocolVersion, PublicKey, SecretKey, Timestamp, U512, }; const ARG_ENTRY_POINT: &str = "entry_point"; @@ -71,14 +74,6 @@ static GENESIS_ROUND_SEIGNIORAGE_RATE: Lazy> = Lazy::new(|| { ) }); -fn get_validator_bid( - builder: &mut LmdbWasmTestBuilder, - validator_public_key: PublicKey, -) -> Option { - let bids = builder.get_bids(); - bids.validator_bid(&validator_public_key) -} - fn get_delegator_bid( builder: &mut LmdbWasmTestBuilder, validator: PublicKey, @@ -132,30 +127,6 @@ fn undelegate( builder.exec(undelegate_request).expect_success().commit(); } -fn get_delegator_staked_amount( - builder: &mut LmdbWasmTestBuilder, - validator_public_key: PublicKey, - delegator_public_key: PublicKey, -) -> U512 { - let bids = builder.get_bids(); - let delegator = bids - .delegator_by_public_keys(&validator_public_key, &delegator_public_key) - .expect("bid should exist for validator-{validator_public_key}, delegator-{delegator_public_key}"); - - delegator.staked_amount() -} - -fn get_era_info(builder: &mut LmdbWasmTestBuilder) -> EraInfo { - let era_info_value = builder - .query(None, Key::EraSummary, &[]) - .expect("should have value"); - - era_info_value - .as_era_info() - .cloned() - .expect("should be era info") -} - #[ignore] #[test] fn should_distribute_delegation_rate_zero() { @@ -3289,7 +3260,7 @@ fn delegator_full_unbond_during_first_reward_era() { // the delegator is scheduled to receive rewards this era. let auction_hash = builder.get_auction_contract_hash(); - let seigniorage_snapshot: SeigniorageRecipientsSnapshot = builder.get_value( + let seigniorage_snapshot: SeigniorageRecipientsSnapshotV2 = builder.get_value( EntityAddr::System(auction_hash.value()), SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, ); diff --git a/execution_engine_testing/tests/src/test/system_contracts/auction/mod.rs b/execution_engine_testing/tests/src/test/system_contracts/auction/mod.rs index 5a142aa7d6..ced556e9a1 100644 --- a/execution_engine_testing/tests/src/test/system_contracts/auction/mod.rs +++ b/execution_engine_testing/tests/src/test/system_contracts/auction/mod.rs @@ -1,2 +1,41 @@ +use casper_engine_test_support::LmdbWasmTestBuilder; +use casper_types::{ + system::auction::{BidsExt, EraInfo, ValidatorBid}, + Key, PublicKey, U512, +}; + mod bids; mod distribute; +mod reservations; + +fn get_validator_bid( + builder: &mut LmdbWasmTestBuilder, + validator_public_key: PublicKey, +) -> Option { + let bids = builder.get_bids(); + bids.validator_bid(&validator_public_key) +} + +pub fn get_delegator_staked_amount( + builder: &mut LmdbWasmTestBuilder, + validator_public_key: PublicKey, + delegator_public_key: PublicKey, +) -> U512 { + let bids = builder.get_bids(); + let delegator = bids + .delegator_by_public_keys(&validator_public_key, &delegator_public_key) + .expect("bid should exist for validator-{validator_public_key}, delegator-{delegator_public_key}"); + + delegator.staked_amount() +} + +pub fn get_era_info(builder: &mut LmdbWasmTestBuilder) -> EraInfo { + let era_info_value = builder + .query(None, Key::EraSummary, &[]) + .expect("should have value"); + + era_info_value + .as_era_info() + .cloned() + .expect("should be era info") +} diff --git a/execution_engine_testing/tests/src/test/system_contracts/auction/reservations.rs b/execution_engine_testing/tests/src/test/system_contracts/auction/reservations.rs new file mode 100644 index 0000000000..4e8640779b --- /dev/null +++ b/execution_engine_testing/tests/src/test/system_contracts/auction/reservations.rs @@ -0,0 +1,973 @@ +use num_rational::Ratio; +use num_traits::{CheckedMul, CheckedSub}; +use once_cell::sync::Lazy; +use std::collections::BTreeMap; +use tempfile::TempDir; + +use casper_engine_test_support::{ + ChainspecConfig, ExecuteRequestBuilder, LmdbWasmTestBuilder, StepRequestBuilder, + DEFAULT_ACCOUNT_ADDR, DEFAULT_PROTOCOL_VERSION, LOCAL_GENESIS_REQUEST, + MINIMUM_ACCOUNT_CREATION_BALANCE, SYSTEM_ADDR, +}; +use casper_execution_engine::{ + engine_state::{engine_config::DEFAULT_MINIMUM_DELEGATION_AMOUNT, Error}, + execution::ExecError, +}; + +use crate::test::system_contracts::auction::{ + get_delegator_staked_amount, get_era_info, get_validator_bid, +}; +use casper_types::{ + self, + account::AccountHash, + api_error::ApiError, + runtime_args, + system::auction::{ + BidsExt, DelegationRate, Error as AuctionError, Reservation, SeigniorageAllocation, + ARG_AMOUNT, ARG_DELEGATION_RATE, ARG_DELEGATOR, ARG_DELEGATORS, ARG_ENTRY_POINT, + ARG_PUBLIC_KEY, ARG_RESERVATIONS, ARG_RESERVED_SLOTS, ARG_REWARDS_MAP, ARG_VALIDATOR, + DELEGATION_RATE_DENOMINATOR, METHOD_DISTRIBUTE, + }, + ProtocolVersion, PublicKey, SecretKey, U512, +}; + +const ARG_TARGET: &str = "target"; + +const CONTRACT_TRANSFER_TO_ACCOUNT: &str = "transfer_to_account_u512.wasm"; +const CONTRACT_ADD_BID: &str = "add_bid.wasm"; +const CONTRACT_DELEGATE: &str = "delegate.wasm"; +const CONTRACT_UNDELEGATE: &str = "undelegate.wasm"; +const CONTRACT_ADD_RESERVATIONS: &str = "add_reservations.wasm"; +const CONTRACT_CANCEL_RESERVATIONS: &str = "cancel_reservations.wasm"; + +const ADD_BID_AMOUNT_1: u64 = 1_000_000_000_000; +const ADD_BID_RESERVED_SLOTS: u32 = 1; + +static VALIDATOR_1: Lazy = Lazy::new(|| { + let secret_key = SecretKey::ed25519_from_bytes([3; SecretKey::ED25519_LENGTH]).unwrap(); + PublicKey::from(&secret_key) +}); +static DELEGATOR_1: Lazy = Lazy::new(|| { + let secret_key = SecretKey::ed25519_from_bytes([205; SecretKey::ED25519_LENGTH]).unwrap(); + PublicKey::from(&secret_key) +}); +static DELEGATOR_2: Lazy = Lazy::new(|| { + let secret_key = SecretKey::ed25519_from_bytes([207; SecretKey::ED25519_LENGTH]).unwrap(); + PublicKey::from(&secret_key) +}); +static DELEGATOR_3: Lazy = Lazy::new(|| { + let secret_key = SecretKey::ed25519_from_bytes([209; SecretKey::ED25519_LENGTH]).unwrap(); + PublicKey::from(&secret_key) +}); +static DELEGATOR_4: Lazy = Lazy::new(|| { + let secret_key = SecretKey::ed25519_from_bytes([211; SecretKey::ED25519_LENGTH]).unwrap(); + PublicKey::from(&secret_key) +}); + +static VALIDATOR_1_ADDR: Lazy = Lazy::new(|| AccountHash::from(&*VALIDATOR_1)); +static DELEGATOR_1_ADDR: Lazy = Lazy::new(|| AccountHash::from(&*DELEGATOR_1)); +static DELEGATOR_2_ADDR: Lazy = Lazy::new(|| AccountHash::from(&*DELEGATOR_2)); +static DELEGATOR_3_ADDR: Lazy = Lazy::new(|| AccountHash::from(&*DELEGATOR_3)); +static DELEGATOR_4_ADDR: Lazy = Lazy::new(|| AccountHash::from(&*DELEGATOR_4)); + +const VALIDATOR_1_DELEGATION_RATE: DelegationRate = 10; +const VALIDATOR_1_RESERVATION_DELEGATION_RATE: DelegationRate = 20; + +/// Fund validator and delegators accounts. +fn setup_accounts(max_delegators_per_validator: u32) -> LmdbWasmTestBuilder { + let chainspec = + ChainspecConfig::default().with_max_delegators_per_validator(max_delegators_per_validator); + + let data_dir = TempDir::new().expect("should create temp dir"); + let mut builder = LmdbWasmTestBuilder::new_with_config(data_dir.path(), chainspec); + + builder.run_genesis(LOCAL_GENESIS_REQUEST.clone()); + + let transfer_to_validator_1 = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *VALIDATOR_1_ADDR, + ARG_AMOUNT => U512::from(MINIMUM_ACCOUNT_CREATION_BALANCE) + }, + ) + .build(); + + let transfer_to_delegator_1 = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *DELEGATOR_1_ADDR, + ARG_AMOUNT => U512::from(MINIMUM_ACCOUNT_CREATION_BALANCE) + }, + ) + .build(); + + let transfer_to_delegator_2 = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *DELEGATOR_2_ADDR, + ARG_AMOUNT => U512::from(MINIMUM_ACCOUNT_CREATION_BALANCE) + }, + ) + .build(); + + let transfer_to_delegator_3 = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *DELEGATOR_3_ADDR, + ARG_AMOUNT => U512::from(MINIMUM_ACCOUNT_CREATION_BALANCE) + }, + ) + .build(); + + let transfer_to_delegator_4 = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *DELEGATOR_4_ADDR, + ARG_AMOUNT => U512::from(MINIMUM_ACCOUNT_CREATION_BALANCE) + }, + ) + .build(); + + let post_genesis_request = vec![ + transfer_to_validator_1, + transfer_to_delegator_1, + transfer_to_delegator_2, + transfer_to_delegator_3, + transfer_to_delegator_4, + ]; + + for request in post_genesis_request { + builder.exec(request).expect_success().commit(); + } + + builder +} + +/// Submit validator bid for `VALIDATOR_1_ADDR` and advance eras +/// until they are elected as active validator. +fn setup_validator_bid(builder: &mut LmdbWasmTestBuilder, reserved_slots: u32) { + let add_validator_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_PUBLIC_KEY => VALIDATOR_1.clone(), + ARG_AMOUNT => U512::from(ADD_BID_AMOUNT_1), + ARG_DELEGATION_RATE => VALIDATOR_1_DELEGATION_RATE, + ARG_RESERVED_SLOTS => reserved_slots, + }, + ) + .build(); + + builder + .exec(add_validator_request) + .expect_success() + .commit(); + + for _ in 0..=builder.get_auction_delay() { + let step_request = StepRequestBuilder::new() + .with_parent_state_hash(builder.get_post_state_hash()) + .with_protocol_version(ProtocolVersion::V1_0_0) + .with_next_era_id(builder.get_era().successor()) + .with_run_auction(true) + .build(); + + assert!( + builder.step(step_request).is_success(), + "must execute step request" + ); + } +} + +#[ignore] +#[test] +fn should_enforce_max_delegators_per_validator_with_reserved_slots() { + let mut builder = setup_accounts(3); + + setup_validator_bid(&mut builder, ADD_BID_RESERVED_SLOTS); + + let delegation_request_1 = ExecuteRequestBuilder::standard( + *DELEGATOR_1_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_1.clone(), + }, + ) + .build(); + + let delegation_request_2 = ExecuteRequestBuilder::standard( + *DELEGATOR_2_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_2.clone(), + }, + ) + .build(); + + let delegation_requests = [delegation_request_1, delegation_request_2]; + + for request in delegation_requests { + builder.exec(request).expect_success().commit(); + } + + // Delegator 3 is not on reservation list and validator is at delegator limit + // therefore delegation request should fail + let delegation_request_3 = ExecuteRequestBuilder::standard( + *DELEGATOR_3_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_3.clone(), + }, + ) + .build(); + + builder.exec(delegation_request_3).expect_failure(); + let error = builder.get_error().expect("should get error"); + + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::ExceededDelegatorSizeLimit as u8)); + + // Once we put Delegator 3 on reserved list the delegation request should succeed + let reservation = Reservation::new(VALIDATOR_1.clone(), DELEGATOR_3.clone(), 0); + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![reservation], + }, + ) + .build(); + builder.exec(reservation_request).expect_success().commit(); + + let delegation_request_4 = ExecuteRequestBuilder::standard( + *DELEGATOR_3_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_3.clone(), + }, + ) + .build(); + builder.exec(delegation_request_4).expect_success().commit(); + + // Delegator 4 not on reserved list and validator at capacity + // therefore delegation request should fail + let delegation_request_5 = ExecuteRequestBuilder::standard( + *DELEGATOR_4_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_4.clone(), + }, + ) + .build(); + builder.exec(delegation_request_5).expect_failure(); + + // Now we undelegate Delegator 3 and cancel his reservation, + // then add reservation for Delegator 4. Then delegation request for + // Delegator 4 should succeed + let undelegation_request = ExecuteRequestBuilder::standard( + *DELEGATOR_3_ADDR, + CONTRACT_UNDELEGATE, + runtime_args! { + ARG_AMOUNT => U512::MAX, + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_3.clone(), + }, + ) + .build(); + builder.exec(undelegation_request).expect_success().commit(); + + let cancellation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_CANCEL_RESERVATIONS, + runtime_args! { + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATORS => vec![DELEGATOR_3.clone()], + }, + ) + .build(); + builder.exec(cancellation_request).expect_success().commit(); + + let reservation = Reservation::new(VALIDATOR_1.clone(), DELEGATOR_4.clone(), 0); + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![reservation], + }, + ) + .build(); + builder.exec(reservation_request).expect_success().commit(); + + let delegation_request_6 = ExecuteRequestBuilder::standard( + *DELEGATOR_4_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_4.clone(), + }, + ) + .build(); + builder.exec(delegation_request_6).expect_success().commit(); +} + +#[ignore] +#[test] +fn should_allow_validator_to_reserve_all_delegator_slots() { + let max_delegators_per_validator = 2; + + let mut builder = setup_accounts(max_delegators_per_validator); + + setup_validator_bid(&mut builder, 0); + + // cannot reserve more slots than maximum delegator number + let add_bid_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_PUBLIC_KEY => VALIDATOR_1.clone(), + ARG_AMOUNT => U512::from(ADD_BID_AMOUNT_1), + ARG_DELEGATION_RATE => VALIDATOR_1_DELEGATION_RATE, + ARG_RESERVED_SLOTS => max_delegators_per_validator + 1, + }, + ) + .build(); + + builder.exec(add_bid_request).expect_failure(); + let error = builder.get_error().expect("should get error"); + + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::ExceededReservationSlotsLimit as u8)); + + // can reserve all slots + let add_bid_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_PUBLIC_KEY => VALIDATOR_1.clone(), + ARG_AMOUNT => U512::from(ADD_BID_AMOUNT_1), + ARG_DELEGATION_RATE => VALIDATOR_1_DELEGATION_RATE, + ARG_RESERVED_SLOTS => max_delegators_per_validator, + }, + ) + .build(); + + builder.exec(add_bid_request).expect_success().commit(); +} + +#[ignore] +#[test] +fn should_not_allow_validator_to_reserve_more_slots_than_free_delegator_slots() { + let max_delegators_per_validator = 2; + + let mut builder = setup_accounts(max_delegators_per_validator); + + setup_validator_bid(&mut builder, 0); + + let delegation_request_1 = ExecuteRequestBuilder::standard( + *DELEGATOR_1_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_1.clone(), + }, + ) + .build(); + + builder.exec(delegation_request_1).expect_success().commit(); + + // cannot reserve more slots than number of free delegator slots + let add_bid_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_PUBLIC_KEY => VALIDATOR_1.clone(), + ARG_AMOUNT => U512::from(ADD_BID_AMOUNT_1), + ARG_DELEGATION_RATE => VALIDATOR_1_DELEGATION_RATE, + ARG_RESERVED_SLOTS => max_delegators_per_validator, + }, + ) + .build(); + + builder.exec(add_bid_request).expect_failure(); + let error = builder.get_error().expect("should get error"); + + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::ExceededReservationSlotsLimit as u8)); +} + +#[ignore] +#[test] +fn should_not_allow_validator_to_reduce_number_of_reserved_spots_if_they_are_occupied() { + let mut builder = setup_accounts(3); + + let reserved_slots = 2; + setup_validator_bid(&mut builder, reserved_slots); + + // add reservations for Delegators 1 and 2 + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_1.clone(), 0), + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_2.clone(), 0), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_success().commit(); + let reservations = builder + .get_bids() + .reservations_by_validator_public_key(&VALIDATOR_1) + .expect("should have reservations"); + assert_eq!(reservations.len(), 2); + + // cannot reduce number of reserved slots because + // there are reservations for all of them + let add_validator_bid_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_PUBLIC_KEY => VALIDATOR_1.clone(), + ARG_AMOUNT => U512::from(ADD_BID_AMOUNT_1), + ARG_DELEGATION_RATE => VALIDATOR_1_DELEGATION_RATE, + ARG_RESERVED_SLOTS => reserved_slots - 1, + }, + ) + .build(); + + builder.exec(add_validator_bid_request).expect_failure(); + let error = builder.get_error().expect("should get error"); + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::ReservationSlotsCountTooSmall as u8)); + + // remove a reservation for Delegator 2 and + // reduce number of reserved spots + let cancellation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_CANCEL_RESERVATIONS, + runtime_args! { + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATORS => vec![DELEGATOR_2.clone()], + }, + ) + .build(); + builder.exec(cancellation_request).expect_success().commit(); + + let reservations = builder + .get_bids() + .reservations_by_validator_public_key(&VALIDATOR_1) + .expect("should have reservations"); + assert_eq!(reservations.len(), 1); + + let add_validator_bid_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_PUBLIC_KEY => VALIDATOR_1.clone(), + ARG_AMOUNT => U512::from(ADD_BID_AMOUNT_1), + ARG_DELEGATION_RATE => VALIDATOR_1_DELEGATION_RATE, + ARG_RESERVED_SLOTS => reserved_slots - 1, + }, + ) + .build(); + + builder + .exec(add_validator_bid_request) + .expect_success() + .commit(); + + // cannot add a reservation for Delegator 2 back + // because number of slots is reduced + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_2.clone(), 0), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_failure(); + let error = builder.get_error().expect("should get error"); + + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::ExceededReservationsLimit as u8)); +} + +#[ignore] +#[test] +fn should_not_allow_validator_to_remove_active_reservation_if_there_are_no_free_delegator_slots() { + let mut builder = setup_accounts(2); + + let reserved_slots = 1; + setup_validator_bid(&mut builder, reserved_slots); + + // add delegation for Delegator 1 + let delegation_request_1 = ExecuteRequestBuilder::standard( + *DELEGATOR_1_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_1.clone(), + }, + ) + .build(); + + builder.exec(delegation_request_1).expect_success().commit(); + + // cannot add delegation for Delegator 2 + let delegation_request_2 = ExecuteRequestBuilder::standard( + *DELEGATOR_2_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_2.clone(), + }, + ) + .build(); + + builder.exec(delegation_request_2).expect_failure(); + let error = builder.get_error().expect("should get error"); + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::ExceededDelegatorSizeLimit as u8)); + + // add reservation for Delegator 2 + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_2.clone(), 0), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_success().commit(); + + // add delegation for Delegator 2 + let delegation_request_2 = ExecuteRequestBuilder::standard( + *DELEGATOR_2_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DEFAULT_MINIMUM_DELEGATION_AMOUNT), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_2.clone(), + }, + ) + .build(); + + builder.exec(delegation_request_2).expect_success().commit(); + + // cannot cancel reservation for Delegator 2 + // because there are no free public slots for delegators + let cancellation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_CANCEL_RESERVATIONS, + runtime_args! { + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATORS => vec![DELEGATOR_2.clone()], + }, + ) + .build(); + builder.exec(cancellation_request).expect_failure(); + let error = builder.get_error().expect("should get error"); + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::ExceededDelegatorSizeLimit as u8)); +} + +#[ignore] +#[test] +fn should_handle_reserved_slots() { + let mut builder = setup_accounts(4); + + let reserved_slots = 3; + setup_validator_bid(&mut builder, reserved_slots); + + let reservations = builder + .get_bids() + .reservations_by_validator_public_key(&VALIDATOR_1); + assert!(reservations.is_none()); + + // add reservations for Delegators 1 and 2 + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_1.clone(), 0), + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_2.clone(), 0), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_success().commit(); + let reservations = builder + .get_bids() + .reservations_by_validator_public_key(&VALIDATOR_1) + .expect("should have reservations"); + assert_eq!(reservations.len(), 2); + + // try to cancel reservation for Delegators 1,2 and 3 + // this fails because reservation for Delegator 3 doesn't exist yet + let cancellation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_CANCEL_RESERVATIONS, + runtime_args! { + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATORS => vec![DELEGATOR_1.clone(), DELEGATOR_2.clone(), DELEGATOR_3.clone()], + }, + ) + .build(); + builder.exec(cancellation_request).expect_failure(); + let error = builder.get_error().expect("should get error"); + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::ReservationNotFound as u8)); + + // add reservation for Delegator 2 and 3 + // reservation for Delegator 2 already exists, but it shouldn't cause an error + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_2.clone(), 0), + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_3.clone(), 0), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_success().commit(); + let reservations = builder + .get_bids() + .reservations_by_validator_public_key(&VALIDATOR_1) + .expect("should have reservations"); + assert_eq!(reservations.len(), 3); + + // try to add reservation for Delegator 4 + // this fails because the reservation list is already full + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_4.clone(), 0), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_failure(); + let error = builder.get_error().expect("should get error"); + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::ExceededReservationsLimit as u8)); + + // cancel all reservations + let cancellation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_CANCEL_RESERVATIONS, + runtime_args! { + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATORS => vec![DELEGATOR_1.clone(), DELEGATOR_2.clone(), DELEGATOR_3.clone()], + }, + ) + .build(); + builder.exec(cancellation_request).expect_success().commit(); + let reservations = builder + .get_bids() + .reservations_by_validator_public_key(&VALIDATOR_1); + assert!(reservations.is_none()); +} + +#[ignore] +#[test] +fn should_update_reservation_delegation_rate() { + let mut builder = setup_accounts(4); + + let reserved_slots = 3; + setup_validator_bid(&mut builder, reserved_slots); + + let reservations = builder + .get_bids() + .reservations_by_validator_public_key(&VALIDATOR_1); + assert!(reservations.is_none()); + + // add reservations for Delegators 1 and 2 + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_1.clone(), 0), + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_2.clone(), 0), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_success().commit(); + let reservations = builder + .get_bids() + .reservations_by_validator_public_key(&VALIDATOR_1) + .expect("should have reservations"); + assert_eq!(reservations.len(), 2); + + // try to change delegation rate for Delegator 1 + // this fails because delegation rate value is invalid + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_1.clone(), DELEGATION_RATE_DENOMINATOR + 1), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_failure(); + let error = builder.get_error().expect("should get error"); + assert!(matches!( + error, + Error::Exec(ExecError::Revert(ApiError::AuctionError(auction_error))) + if auction_error == AuctionError::DelegationRateTooLarge as u8)); + + // change delegation rate for Delegator 1 + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_1.clone(), 10), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_success().commit(); + let reservations = builder + .get_bids() + .reservations_by_validator_public_key(&VALIDATOR_1) + .expect("should have reservations"); + assert_eq!(reservations.len(), 2); + + let delegator_1_reservation = reservations + .iter() + .find(|r| *r.delegator_public_key() == *DELEGATOR_1) + .unwrap(); + assert_eq!(*delegator_1_reservation.delegation_rate(), 10); +} + +#[ignore] +#[test] +fn should_distribute_rewards_with_reserved_slots() { + let validator_stake = U512::from(ADD_BID_AMOUNT_1); + let delegator_1_stake = U512::from(1_000_000_000_000u64); + let delegator_2_stake = U512::from(1_000_000_000_000u64); + let total_delegator_stake = delegator_1_stake + delegator_2_stake; + let total_stake = validator_stake + total_delegator_stake; + + let mut builder = setup_accounts(3); + + setup_validator_bid(&mut builder, ADD_BID_RESERVED_SLOTS); + + // add reservation for Delegator 1 + let reservation_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_RESERVATIONS, + runtime_args! { + ARG_RESERVATIONS => vec![ + Reservation::new(VALIDATOR_1.clone(), DELEGATOR_1.clone(), VALIDATOR_1_RESERVATION_DELEGATION_RATE), + ], + }, + ) + .build(); + builder.exec(reservation_request).expect_success().commit(); + + // add delegator bids for Delegator 1 and 2 + let delegation_request_1 = ExecuteRequestBuilder::standard( + *DELEGATOR_1_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => delegator_1_stake, + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_1.clone(), + }, + ) + .build(); + let delegation_request_2 = ExecuteRequestBuilder::standard( + *DELEGATOR_2_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => delegator_2_stake, + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_2.clone(), + }, + ) + .build(); + + let delegation_requests = [delegation_request_1, delegation_request_2]; + + for request in delegation_requests { + builder.exec(request).expect_success().commit(); + } + + // calculate expected rewards + let protocol_version = DEFAULT_PROTOCOL_VERSION; + let initial_supply = builder.total_supply(protocol_version, None); + let total_payout = builder.base_round_reward(None, protocol_version); + let rate = builder.round_seigniorage_rate(None, protocol_version); + let expected_total_reward = rate * initial_supply; + let expected_total_reward_integer = expected_total_reward.to_integer(); + assert_eq!(total_payout, expected_total_reward_integer); + + // advance eras + for _ in 0..=builder.get_auction_delay() { + let step_request = StepRequestBuilder::new() + .with_parent_state_hash(builder.get_post_state_hash()) + .with_protocol_version(ProtocolVersion::V1_0_0) + .with_next_era_id(builder.get_era().successor()) + .with_run_auction(true) + .build(); + + assert!( + builder.step(step_request).is_success(), + "must execute step successfully" + ); + } + + let mut rewards = BTreeMap::new(); + rewards.insert(VALIDATOR_1.clone(), vec![total_payout]); + + let distribute_request = ExecuteRequestBuilder::contract_call_by_hash( + *SYSTEM_ADDR, + builder.get_auction_contract_hash(), + METHOD_DISTRIBUTE, + runtime_args! { + ARG_ENTRY_POINT => METHOD_DISTRIBUTE, + ARG_REWARDS_MAP => rewards + }, + ) + .build(); + + builder.exec(distribute_request).commit().expect_success(); + + let default_commission_rate = Ratio::new( + U512::from(VALIDATOR_1_DELEGATION_RATE), + U512::from(DELEGATION_RATE_DENOMINATOR), + ); + let reservation_commission_rate = Ratio::new( + U512::from(VALIDATOR_1_RESERVATION_DELEGATION_RATE), + U512::from(DELEGATION_RATE_DENOMINATOR), + ); + let reward_multiplier = Ratio::new(total_delegator_stake, total_stake); + let base_delegator_reward = expected_total_reward + .checked_mul(&reward_multiplier) + .expect("must get delegator reward"); + + let delegator_1_expected_payout = { + let reward_multiplier = Ratio::new(delegator_1_stake, total_delegator_stake); + let delegator_1_reward = base_delegator_reward + .checked_mul(&reward_multiplier) + .unwrap(); + let commission = delegator_1_reward + .checked_mul(&reservation_commission_rate) + .unwrap(); + delegator_1_reward + .checked_sub(&commission) + .unwrap() + .to_integer() + }; + let delegator_2_expected_payout = { + let reward_multiplier = Ratio::new(delegator_2_stake, total_delegator_stake); + let delegator_2_reward = base_delegator_reward + .checked_mul(&reward_multiplier) + .unwrap(); + let commission = delegator_2_reward + .checked_mul(&default_commission_rate) + .unwrap(); + delegator_2_reward + .checked_sub(&commission) + .unwrap() + .to_integer() + }; + + let delegator_1_actual_payout = { + let delegator_stake_before = delegator_1_stake; + let delegator_stake_after = + get_delegator_staked_amount(&mut builder, VALIDATOR_1.clone(), DELEGATOR_1.clone()); + delegator_stake_after - delegator_stake_before + }; + assert_eq!(delegator_1_actual_payout, delegator_1_expected_payout); + + let delegator_2_actual_payout = { + let delegator_stake_before = delegator_2_stake; + let delegator_stake_after = + get_delegator_staked_amount(&mut builder, VALIDATOR_1.clone(), DELEGATOR_2.clone()); + delegator_stake_after - delegator_stake_before + }; + assert_eq!(delegator_2_actual_payout, delegator_2_expected_payout); + + let validator_1_expected_payout = { + let total_delegator_payout = delegator_1_expected_payout + delegator_2_expected_payout; + let validators_part = expected_total_reward - Ratio::from(total_delegator_payout); + validators_part.to_integer() + }; + + let validator_1_actual_payout = { + let validator_stake_before = validator_stake; + let validator_stake_after = get_validator_bid(&mut builder, VALIDATOR_1.clone()) + .expect("should have validator bid") + .staked_amount(); + validator_stake_after - validator_stake_before + }; + + assert_eq!(validator_1_actual_payout, validator_1_expected_payout); + + let era_info = get_era_info(&mut builder); + + assert!(matches!( + era_info.select(DELEGATOR_1.clone()).next(), + Some(SeigniorageAllocation::Delegator { delegator_public_key, amount, .. }) + if *delegator_public_key == *DELEGATOR_1 && *amount == delegator_1_expected_payout + )); + + assert!(matches!( + era_info.select(DELEGATOR_2.clone()).next(), + Some(SeigniorageAllocation::Delegator { delegator_public_key, amount, .. }) + if *delegator_public_key == *DELEGATOR_2 && *amount == delegator_2_expected_payout + )); +} diff --git a/execution_engine_testing/tests/src/test/system_contracts/upgrade.rs b/execution_engine_testing/tests/src/test/system_contracts/upgrade.rs index 8a0980834b..9f29a5976a 100644 --- a/execution_engine_testing/tests/src/test/system_contracts/upgrade.rs +++ b/execution_engine_testing/tests/src/test/system_contracts/upgrade.rs @@ -16,7 +16,10 @@ use casper_types::{ system::{ self, auction::{ - AUCTION_DELAY_KEY, LOCKED_FUNDS_PERIOD_KEY, UNBONDING_DELAY_KEY, VALIDATOR_SLOTS_KEY, + SeigniorageRecipientsSnapshotV1, SeigniorageRecipientsSnapshotV2, AUCTION_DELAY_KEY, + DEFAULT_SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION, LOCKED_FUNDS_PERIOD_KEY, + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY, + UNBONDING_DELAY_KEY, VALIDATOR_SLOTS_KEY, }, mint::ROUND_SEIGNIORAGE_RATE_KEY, }, @@ -778,3 +781,110 @@ fn should_upgrade_legacy_accounts() { .upgrade(&mut upgrade_request) .expect_upgrade_success(); } + +#[ignore] +#[test] +fn should_migrate_seigniorage_snapshot_to_new_version() { + let (mut builder, lmdb_fixture_state, _temp_dir) = + lmdb_fixture::builder_from_global_state_fixture(lmdb_fixture::RELEASE_1_4_3); + + let auction_contract_hash = builder.get_auction_contract_hash(); + + // get legacy auction contract + let auction_contract = builder + .query(None, Key::Hash(auction_contract_hash.value()), &[]) + .expect("should have auction contract") + .into_contract() + .expect("should have legacy Contract under the Key::Contract variant"); + + // check that snapshot version key does not exist yet + let auction_named_keys = auction_contract.named_keys(); + let maybe_snapshot_version_named_key = + auction_named_keys.get(SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY); + assert!(maybe_snapshot_version_named_key.is_none()); + + // fetch legacy snapshot + let legacy_seigniorage_snapshot: SeigniorageRecipientsSnapshotV1 = { + let snapshot_key = auction_named_keys + .get(SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY) + .expect("snapshot named key should exist"); + builder + .query(None, *snapshot_key, &[]) + .expect("should have seigniorage snapshot") + .as_cl_value() + .expect("should be a CLValue") + .clone() + .into_t() + .expect("should be SeigniorageRecipientsSnapshotV1") + }; + + // prepare upgrade request + let old_protocol_version = lmdb_fixture_state.genesis_protocol_version(); + let mut upgrade_request = UpgradeRequestBuilder::new() + .with_current_protocol_version(old_protocol_version) + .with_new_protocol_version(ProtocolVersion::from_parts(2, 0, 0)) + .with_activation_point(DEFAULT_ACTIVATION_POINT) + .build(); + + // execute upgrade + builder + .upgrade(&mut upgrade_request) + .expect_upgrade_success(); + + // fetch updated named keys + let auction_named_keys = + builder.get_named_keys(EntityAddr::System(auction_contract_hash.value())); + + // check that snapshot version named key was populated + let snapshot_version_key = auction_named_keys + .get(SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY) + .expect("auction should have snapshot version named key"); + let snapshot_version: u8 = builder + .query(None, *snapshot_version_key, &[]) + .expect("should have seigniorage snapshot version") + .as_cl_value() + .expect("should be a CLValue") + .clone() + .into_t() + .expect("should be u8"); + assert_eq!( + snapshot_version, + DEFAULT_SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION + ); + + // fetch new snapshot + let seigniorage_snapshot: SeigniorageRecipientsSnapshotV2 = { + let snapshot_key = auction_named_keys + .get(SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY) + .expect("snapshot named key should exist"); + builder + .query(None, *snapshot_key, &[]) + .expect("should have seigniorage snapshot") + .as_cl_value() + .expect("should be a CLValue") + .clone() + .into_t() + .expect("should be SeigniorageRecipientsSnapshotV2") + }; + + // compare snapshots + for era_id in legacy_seigniorage_snapshot.keys() { + let legacy_seigniorage_recipients = legacy_seigniorage_snapshot.get(era_id).unwrap(); + let new_seigniorage_recipient = seigniorage_snapshot.get(era_id).unwrap(); + + for pubkey in legacy_seigniorage_recipients.keys() { + let legacy_recipient = legacy_seigniorage_recipients.get(pubkey).unwrap(); + let new_recipient = new_seigniorage_recipient.get(pubkey).unwrap(); + + assert_eq!(legacy_recipient.stake(), new_recipient.stake()); + assert_eq!( + legacy_recipient.delegation_rate(), + new_recipient.delegation_rate() + ); + assert_eq!( + legacy_recipient.delegator_stake(), + new_recipient.delegator_stake() + ); + } + } +} diff --git a/node/src/components/binary_port.rs b/node/src/components/binary_port.rs index 1040f093f6..67b2a7b5d3 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -1215,9 +1215,9 @@ where return BinaryResponse::new_empty(protocol_version); }; - let seigniorage_recipient = snapshot - .get(&header.era_id()) - .and_then(|era| era.get(&validator)); + let seigniorage_recipient = + snapshot.get_seignorage_recipient(&header.era_id(), &validator); + let reward = auction::reward( &validator, delegator.as_deref(), diff --git a/smart_contracts/contracts/client/add-reservations/src/main.rs b/smart_contracts/contracts/client/add-reservations/src/main.rs index 516c51b89d..96177b127f 100644 --- a/smart_contracts/contracts/client/add-reservations/src/main.rs +++ b/smart_contracts/contracts/client/add-reservations/src/main.rs @@ -5,11 +5,18 @@ extern crate alloc; use alloc::vec::Vec; -use casper_contract::contract_api::runtime; -use casper_types::system::auction::{Reservation, ARG_RESERVATIONS}; +use casper_contract::contract_api::{runtime, system}; +use casper_types::{ + runtime_args, + system::auction::{self, Reservation}, +}; -fn add_reservations(_reservations: Vec) { - todo!(); +fn add_reservations(reservations: Vec) { + let contract_hash = system::get_auction(); + let args = runtime_args! { + auction::ARG_RESERVATIONS => reservations, + }; + runtime::call_contract::<()>(contract_hash, auction::METHOD_ADD_RESERVATIONS, args); } // Add delegators to validator's reserved list. @@ -18,7 +25,7 @@ fn add_reservations(_reservations: Vec) { // Issues an add_reservations request to the auction contract. #[no_mangle] pub extern "C" fn call() { - let reservations: Vec = runtime::get_named_arg(ARG_RESERVATIONS); + let reservations: Vec = runtime::get_named_arg(auction::ARG_RESERVATIONS); add_reservations(reservations); } diff --git a/smart_contracts/contracts/client/cancel-reservations/src/main.rs b/smart_contracts/contracts/client/cancel-reservations/src/main.rs index 112f25bfd6..c6d3f20082 100644 --- a/smart_contracts/contracts/client/cancel-reservations/src/main.rs +++ b/smart_contracts/contracts/client/cancel-reservations/src/main.rs @@ -5,14 +5,16 @@ extern crate alloc; use alloc::vec::Vec; -use casper_contract::contract_api::runtime; -use casper_types::{ - system::auction::{ARG_DELEGATORS, ARG_VALIDATOR}, - PublicKey, -}; +use casper_contract::contract_api::{runtime, system}; +use casper_types::{runtime_args, system::auction, PublicKey}; -fn cancel_reservations(_validator: PublicKey, _delegators: Vec) { - todo!(); +fn cancel_reservations(validator: PublicKey, delegators: Vec) { + let contract_hash = system::get_auction(); + let args = runtime_args! { + auction::ARG_VALIDATOR => validator, + auction::ARG_DELEGATORS => delegators, + }; + runtime::call_contract::<()>(contract_hash, auction::METHOD_CANCEL_RESERVATIONS, args); } // Remove delegators from validator's reserved list. @@ -21,8 +23,8 @@ fn cancel_reservations(_validator: PublicKey, _delegators: Vec) { // Issues a cancel_reservations request to the auction contract. #[no_mangle] pub extern "C" fn call() { - let delegators: Vec = runtime::get_named_arg(ARG_DELEGATORS); - let validator = runtime::get_named_arg(ARG_VALIDATOR); + let delegators: Vec = runtime::get_named_arg(auction::ARG_DELEGATORS); + let validator = runtime::get_named_arg(auction::ARG_VALIDATOR); cancel_reservations(validator, delegators); } diff --git a/storage/src/data_access_layer/auction.rs b/storage/src/data_access_layer/auction.rs index c947547d51..f100f2fa5a 100644 --- a/storage/src/data_access_layer/auction.rs +++ b/storage/src/data_access_layer/auction.rs @@ -8,7 +8,10 @@ use casper_types::{ account::AccountHash, bytesrepr::FromBytes, execution::Effects, - system::{auction, auction::DelegationRate}, + system::{ + auction, + auction::{DelegationRate, Reservation}, + }, CLTyped, CLValue, CLValueError, Chainspec, Digest, InitiatorAddr, ProtocolVersion, PublicKey, RuntimeArgs, TransactionEntryPoint, TransactionHash, U512, }; @@ -103,6 +106,20 @@ pub enum AuctionMethod { /// New public key. new_public_key: PublicKey, }, + /// Add delegator slot reservations. + AddReservations { + /// List of reservations. + reservations: Vec, + }, + /// Remove delegator slot reservations for delegators with specified public keys. + CancelReservations { + /// Validator public key. + validator: PublicKey, + /// List of delegator public keys. + delegators: Vec, + /// Max delegators per validator. + max_delegators_per_validator: u32, + }, } impl AuctionMethod { @@ -134,9 +151,11 @@ impl AuctionMethod { TransactionEntryPoint::ChangeBidPublicKey => { Self::new_change_bid_public_key(runtime_args) } - TransactionEntryPoint::AddReservations | TransactionEntryPoint::CancelReservations => { - todo!() - } + TransactionEntryPoint::AddReservations => Self::new_add_reservations(runtime_args), + TransactionEntryPoint::CancelReservations => Self::new_cancel_reservations( + runtime_args, + chainspec.core_config.max_delegators_per_validator, + ), } } @@ -227,6 +246,26 @@ impl AuctionMethod { }) } + fn new_add_reservations(runtime_args: &RuntimeArgs) -> Result { + let reservations = Self::get_named_argument(runtime_args, auction::ARG_RESERVATIONS)?; + + Ok(Self::AddReservations { reservations }) + } + + fn new_cancel_reservations( + runtime_args: &RuntimeArgs, + max_delegators_per_validator: u32, + ) -> Result { + let validator = Self::get_named_argument(runtime_args, auction::ARG_VALIDATOR)?; + let delegators = Self::get_named_argument(runtime_args, auction::ARG_DELEGATORS)?; + + Ok(Self::CancelReservations { + validator, + delegators, + max_delegators_per_validator, + }) + } + fn get_named_argument( args: &RuntimeArgs, name: &str, diff --git a/storage/src/data_access_layer/era_validators.rs b/storage/src/data_access_layer/era_validators.rs index 481b8fd7a3..506480f479 100644 --- a/storage/src/data_access_layer/era_validators.rs +++ b/storage/src/data_access_layer/era_validators.rs @@ -31,7 +31,7 @@ impl EraValidatorsRequest { } } -/// Result enum that represents all possible outcomes of a balance request. +/// Result enum that represents all possible outcomes of a era validators request. #[derive(Debug)] pub enum EraValidatorsResult { /// Returned if auction is not found. This is a catastrophic outcome. diff --git a/storage/src/data_access_layer/seigniorage_recipients.rs b/storage/src/data_access_layer/seigniorage_recipients.rs index 03a99bb2e2..721b3d34be 100644 --- a/storage/src/data_access_layer/seigniorage_recipients.rs +++ b/storage/src/data_access_layer/seigniorage_recipients.rs @@ -31,7 +31,7 @@ impl SeigniorageRecipientsRequest { } } -/// Result enum that represents all possible outcomes of a balance request. +/// Result enum that represents all possible outcomes of a seignorage recipients request. #[derive(Debug)] pub enum SeigniorageRecipientsResult { /// Returned if auction is not found. This is a catastrophic outcome. diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index a93b9dffe3..5b6865b452 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -23,7 +23,10 @@ use casper_types::{ global_state::TrieMerkleProof, system::{ self, - auction::{ERA_END_TIMESTAMP_MILLIS_KEY, ERA_ID_KEY, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY}, + auction::{ + SeigniorageRecipientsSnapshot, ERA_END_TIMESTAMP_MILLIS_KEY, ERA_ID_KEY, + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY, + }, mint::{ BalanceHoldAddr, BalanceHoldAddrTag, ARG_AMOUNT, ROUND_SEIGNIORAGE_RATE_KEY, TOTAL_SUPPLY_KEY, @@ -1043,8 +1046,14 @@ pub trait StateProvider { SeigniorageRecipientsResult::Success { seigniorage_recipients, } => { - let era_validators = - auction::detail::era_validators_from_snapshot(seigniorage_recipients); + let era_validators = match seigniorage_recipients { + SeigniorageRecipientsSnapshot::V1(snapshot) => { + auction::detail::era_validators_from_legacy_snapshot(snapshot) + } + SeigniorageRecipientsSnapshot::V2(snapshot) => { + auction::detail::era_validators_from_snapshot(snapshot) + } + }; EraValidatorsResult::Success { era_validators } } } @@ -1064,26 +1073,64 @@ pub trait StateProvider { } }; - let query_request = match tc.get_system_entity_registry() { - Ok(scr) => match scr.get(AUCTION).copied() { - Some(auction_hash) => { - let key = if request.protocol_version().value().major < 2 { - Key::Hash(auction_hash.value()) - } else { - Key::addressable_entity_key(EntityKindTag::System, auction_hash) - }; - QueryRequest::new( - state_hash, - key, - vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.to_string()], - ) + let (snapshot_query_request, snapshot_version_query_request) = + match tc.get_system_entity_registry() { + Ok(scr) => match scr.get(AUCTION).copied() { + Some(auction_hash) => { + let key = if request.protocol_version().value().major < 2 { + Key::Hash(auction_hash.value()) + } else { + Key::addressable_entity_key(EntityKindTag::System, auction_hash) + }; + ( + QueryRequest::new( + state_hash, + key, + vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.to_string()], + ), + QueryRequest::new( + state_hash, + key, + vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY.to_string()], + ), + ) + } + None => return SeigniorageRecipientsResult::AuctionNotFound, + }, + Err(err) => return SeigniorageRecipientsResult::Failure(err), + }; + + // check if snapshot version flag is present + let snapshot_version: Option = match self.query(snapshot_version_query_request) { + QueryResult::RootNotFound => return SeigniorageRecipientsResult::RootNotFound, + QueryResult::Failure(error) => { + error!(?error, "unexpected tracking copy error"); + return SeigniorageRecipientsResult::Failure(error); + } + QueryResult::ValueNotFound(_msg) => None, + QueryResult::Success { value, proofs: _ } => { + let cl_value = match value.into_cl_value() { + Some(snapshot_version_cl_value) => snapshot_version_cl_value, + None => { + error!("unexpected query failure; seigniorage recipients snapshot version is not a CLValue"); + return SeigniorageRecipientsResult::Failure( + TrackingCopyError::UnexpectedStoredValueVariant, + ); + } + }; + + match cl_value.into_t() { + Ok(snapshot_version) => Some(snapshot_version), + Err(cve) => { + return SeigniorageRecipientsResult::Failure(TrackingCopyError::CLValue( + cve, + )); + } } - None => return SeigniorageRecipientsResult::AuctionNotFound, - }, - Err(err) => return SeigniorageRecipientsResult::Failure(err), + } }; - let snapshot = match self.query(query_request) { + let snapshot = match self.query(snapshot_query_request) { QueryResult::RootNotFound => return SeigniorageRecipientsResult::RootNotFound, QueryResult::Failure(error) => { error!(?error, "unexpected tracking copy error"); @@ -1104,12 +1151,30 @@ pub trait StateProvider { } }; - match cl_value.into_t() { - Ok(snapshot) => snapshot, - Err(cve) => { - return SeigniorageRecipientsResult::Failure(TrackingCopyError::CLValue( - cve, - )); + match snapshot_version { + Some(_) => { + let snapshot = match cl_value.into_t() { + Ok(snapshot) => snapshot, + Err(cve) => { + error!("Failed to convert snapshot from CLValue"); + return SeigniorageRecipientsResult::Failure( + TrackingCopyError::CLValue(cve), + ); + } + }; + SeigniorageRecipientsSnapshot::V2(snapshot) + } + None => { + let snapshot = match cl_value.into_t() { + Ok(snapshot) => snapshot, + Err(cve) => { + error!("Failed to convert snapshot from CLValue"); + return SeigniorageRecipientsResult::Failure( + TrackingCopyError::CLValue(cve), + ); + } + }; + SeigniorageRecipientsSnapshot::V1(snapshot) } } } @@ -1235,6 +1300,8 @@ pub trait StateProvider { } }; + let max_delegators_per_validator = config.max_delegators_per_validator(); + let mut runtime = RuntimeNative::new( config, protocol_version, @@ -1270,6 +1337,7 @@ pub trait StateProvider { minimum_delegation_amount, maximum_delegation_amount, 0, + max_delegators_per_validator, ) .map(AuctionMethodRet::UpdatedAmount) .map_err(TrackingCopyError::Api), @@ -1318,6 +1386,22 @@ pub trait StateProvider { .map_err(|auc_err| { TrackingCopyError::SystemContract(system::Error::Auction(auc_err)) }), + AuctionMethod::AddReservations { reservations } => runtime + .add_reservations(reservations) + .map(|_| AuctionMethodRet::Unit) + .map_err(|auc_err| { + TrackingCopyError::SystemContract(system::Error::Auction(auc_err)) + }), + AuctionMethod::CancelReservations { + validator, + delegators, + max_delegators_per_validator, + } => runtime + .cancel_reservations(validator, delegators, max_delegators_per_validator) + .map(|_| AuctionMethodRet::Unit) + .map_err(|auc_err| { + TrackingCopyError::SystemContract(system::Error::Auction(auc_err)) + }), }; let effects = tc.borrow_mut().effects(); diff --git a/storage/src/system/auction.rs b/storage/src/system/auction.rs index df2a9ab440..0b93e20345 100644 --- a/storage/src/system/auction.rs +++ b/storage/src/system/auction.rs @@ -18,9 +18,10 @@ use crate::system::auction::detail::{ use casper_types::{ account::AccountHash, system::auction::{ - BidAddr, BidKind, Bridge, DelegationRate, EraInfo, EraValidators, Error, - SeigniorageRecipients, SeigniorageRecipientsSnapshot, UnbondingPurse, ValidatorBid, - ValidatorCredit, ValidatorWeights, DELEGATION_RATE_DENOMINATOR, + BidAddr, BidKind, Bridge, DelegationRate, EraInfo, EraValidators, Error, Reservation, + SeigniorageRecipient, SeigniorageRecipientsSnapshot, SeigniorageRecipientsV2, + UnbondingPurse, ValidatorBid, ValidatorCredit, ValidatorWeights, + DELEGATION_RATE_DENOMINATOR, }, ApiError, EraId, Key, PublicKey, U512, }; @@ -43,7 +44,7 @@ pub trait Auction: /// rates and lists of delegators together with their delegated quantities from delegators. /// This function is publicly accessible, but intended for system use by the Handle Payment /// contract, because this data is necessary for distributing seigniorage. - fn read_seigniorage_recipients(&mut self) -> Result { + fn read_seigniorage_recipients(&mut self) -> Result { // `era_validators` are assumed to be computed already by calling "run_auction" entrypoint. let era_index = detail::get_era_id(self)?; let mut seigniorage_recipients_snapshot = @@ -68,6 +69,7 @@ pub trait Auction: /// [`DELEGATION_RATE_DENOMINATOR`]. /// /// Returns a [`U512`] value indicating total amount of tokens staked for given `public_key`. + #[allow(clippy::too_many_arguments)] fn add_bid( &mut self, public_key: PublicKey, @@ -75,8 +77,8 @@ pub trait Auction: amount: U512, minimum_delegation_amount: u64, maximum_delegation_amount: u64, - // TODO: reservation list functionality implementation - _reserved_slots: u32, + reserved_slots: u32, + max_delegators_per_validator: u32, ) -> Result { if !self.allow_auction_bids() { // The validator set may be closed on some side chains, @@ -92,6 +94,10 @@ pub trait Auction: return Err(Error::DelegationRateTooLarge.into()); } + if reserved_slots > max_delegators_per_validator { + return Err(Error::ExceededReservationSlotsLimit.into()); + } + let provided_account_hash = AccountHash::from_public_key(&public_key, |x| self.blake2b(x)); if !self.is_allowed_session_caller(&provided_account_hash) { @@ -102,6 +108,7 @@ pub trait Auction: let (target, validator_bid) = if let Some(BidKind::Validator(mut validator_bid)) = self.read_bid(&validator_bid_key)? { + // update an existing validator bid if validator_bid.inactive() { validator_bid.activate(); } @@ -111,8 +118,29 @@ pub trait Auction: minimum_delegation_amount, maximum_delegation_amount, ); + // perform validation if number of reserved slots has changed + if reserved_slots != validator_bid.reserved_slots() { + let validator_bid_addr = BidAddr::from(public_key.clone()); + // cannot reserve fewer slots than there are reservations + let reservation_count = self.reservation_count(&validator_bid_addr)?; + if reserved_slots < reservation_count as u32 { + return Err(Error::ReservationSlotsCountTooSmall.into()); + } + + // cannot reserve more slots than there are free delegator slots + let used_reservation_count = self.used_reservation_count(&validator_bid_addr)?; + let delegator_count = self.delegator_count(&validator_bid_addr)?; + let normal_delegators = delegator_count - used_reservation_count; + let max_reserved_slots = max_delegators_per_validator - normal_delegators as u32; + if reserved_slots > max_reserved_slots { + return Err(Error::ExceededReservationSlotsLimit.into()); + } + validator_bid.with_reserved_slots(reserved_slots); + } + (*validator_bid.bonding_purse(), validator_bid) } else { + // create new validator bid let bonding_purse = self.create_purse()?; let validator_bid = ValidatorBid::unlocked( public_key, @@ -121,6 +149,7 @@ pub trait Auction: delegation_rate, minimum_delegation_amount, maximum_delegation_amount, + reserved_slots, ); (bonding_purse, Box::new(validator_bid)) }; @@ -459,6 +488,53 @@ pub trait Auction: Ok(()) } + /// Adds new reservations for a given validator with specified delegator public keys + /// and delegation rates. If during adding reservations configured number of reserved + /// delegator slots is exceeded it returns an error. + /// + /// If given reservation exists already and the delegation rate was changed it's updated. + fn add_reservations(&mut self, reservations: Vec) -> Result<(), Error> { + if !self.allow_auction_bids() { + // Validation set rotation might be disabled on some private chains and we should not + // allow new bids to come in. + return Err(Error::AuctionBidsDisabled); + } + + for reservation in reservations { + if !self + .is_allowed_session_caller(&AccountHash::from(reservation.validator_public_key())) + { + return Err(Error::InvalidContext); + } + + detail::handle_add_reservation(self, reservation)?; + } + Ok(()) + } + + /// Removes reservations for given delegator public keys. If a reservation for one of the keys + /// does not exist it returns an error. + fn cancel_reservations( + &mut self, + validator: PublicKey, + delegators: Vec, + max_delegators_per_validator: u32, + ) -> Result<(), Error> { + if !self.is_allowed_session_caller(&AccountHash::from(&validator)) { + return Err(Error::InvalidContext); + } + + for delegator in delegators { + detail::handle_cancel_reservation( + self, + validator.clone(), + delegator.clone(), + max_delegators_per_validator, + )?; + } + Ok(()) + } + /// Slashes each validator. /// /// This can be only invoked through a system call. @@ -634,7 +710,7 @@ pub trait Auction: } }; - let (validator_bids, validator_credits, delegator_bids) = + let (validator_bids, validator_credits, delegator_bids, reservations) = validator_bids_detail.destructure(); // call prune BEFORE incrementing the era @@ -650,7 +726,8 @@ pub trait Auction: // Update seigniorage recipients for current era { let mut snapshot = detail::get_seigniorage_recipients_snapshot(self)?; - let recipients = seigniorage_recipients(&winners, &validator_bids, &delegator_bids)?; + let recipients = + seigniorage_recipients(&winners, &validator_bids, &delegator_bids, &reservations)?; let previous_recipients = snapshot.insert(delayed_era, recipients); assert!(previous_recipients.is_none()); @@ -696,7 +773,7 @@ pub trait Auction: &proposer, current_era_id, &amounts, - &seigniorage_recipients_snapshot, + &SeigniorageRecipientsSnapshot::V2(seigniorage_recipients_snapshot.clone()), ) .map(|infos| infos.into_iter().map(move |info| (proposer.clone(), info))) }) @@ -954,12 +1031,24 @@ fn rewards_per_validator( let rewarded_era = era_id .checked_sub(eras_back) .ok_or(Error::MissingSeigniorageRecipients)?; - let Some(recipient) = seigniorage_recipients_snapshot - .get(&rewarded_era) - .ok_or(Error::MissingSeigniorageRecipients)? - .get(validator) - .cloned() - else { + + // try to find validator in seigniorage snapshot + let maybe_seigniorage_recipient = match seigniorage_recipients_snapshot { + SeigniorageRecipientsSnapshot::V1(snapshot) => snapshot + .get(&rewarded_era) + .ok_or(Error::MissingSeigniorageRecipients)? + .get(validator) + .cloned() + .map(SeigniorageRecipient::V1), + SeigniorageRecipientsSnapshot::V2(snapshot) => snapshot + .get(&rewarded_era) + .ok_or(Error::MissingSeigniorageRecipients)? + .get(validator) + .cloned() + .map(SeigniorageRecipient::V2), + }; + + let Some(recipient) = maybe_seigniorage_recipient else { // We couldn't find the validator. If the reward amount is zero, we don't care - // the validator wasn't supposed to be rewarded in this era, anyway. Otherwise, // return an error. @@ -989,32 +1078,37 @@ fn rewards_per_validator( .delegator_total_stake() .ok_or(Error::ArithmeticOverflow)?; - let delegators_part: Ratio = { + // calculate part of reward to be distributed to delegators before commission + let base_delegators_part: Ratio = { + let reward_multiplier: Ratio = Ratio::new(delegator_total_stake, total_stake); + total_reward + .checked_mul(&reward_multiplier) + .ok_or(Error::ArithmeticOverflow)? + }; + + let default = BTreeMap::new(); + let reservation_delegation_rates = + recipient.reservation_delegation_rates().unwrap_or(&default); + // calculate commission and final reward for each delegator + let mut delegator_rewards: BTreeMap = BTreeMap::new(); + for (delegator_key, delegator_stake) in recipient.delegator_stake().iter() { + let reward_multiplier = Ratio::new(*delegator_stake, delegator_total_stake); + let base_reward = base_delegators_part * reward_multiplier; + let delegation_rate = *reservation_delegation_rates + .get(delegator_key) + .unwrap_or(recipient.delegation_rate()); let commission_rate = Ratio::new( - U512::from(*recipient.delegation_rate()), + U512::from(delegation_rate), U512::from(DELEGATION_RATE_DENOMINATOR), ); - let reward_multiplier: Ratio = Ratio::new(delegator_total_stake, total_stake); - let delegator_reward: Ratio = total_reward - .checked_mul(&reward_multiplier) - .ok_or(Error::ArithmeticOverflow)?; - let commission: Ratio = delegator_reward + let commission: Ratio = base_reward .checked_mul(&commission_rate) .ok_or(Error::ArithmeticOverflow)?; - delegator_reward + let reward = base_reward .checked_sub(&commission) - .ok_or(Error::ArithmeticOverflow)? - }; - - let delegator_rewards: BTreeMap = recipient - .delegator_stake() - .iter() - .map(|(delegator_key, delegator_stake)| { - let reward_multiplier = Ratio::new(*delegator_stake, delegator_total_stake); - let reward = delegators_part * reward_multiplier; - (delegator_key.clone(), reward.to_integer()) - }) - .collect(); + .ok_or(Error::ArithmeticOverflow)?; + delegator_rewards.insert(delegator_key.clone(), reward.to_integer()); + } let total_delegator_payout: U512 = delegator_rewards.iter().map(|(_, &amount)| amount).sum(); diff --git a/storage/src/system/auction/auction_native.rs b/storage/src/system/auction/auction_native.rs index 5b16fd2002..40d3d6fee2 100644 --- a/storage/src/system/auction/auction_native.rs +++ b/storage/src/system/auction/auction_native.rs @@ -73,6 +73,55 @@ where Ok(keys.len()) } + fn reservation_count(&mut self, bid_addr: &BidAddr) -> Result { + let reservation_prefix = bid_addr.reservation_prefix()?; + let reservation_keys = self + .get_keys_by_prefix(&reservation_prefix) + .map_err(|err| { + error!("RuntimeProvider::reservation_count {:?}", err); + Error::Storage + })?; + Ok(reservation_keys.len()) + } + + fn used_reservation_count(&mut self, bid_addr: &BidAddr) -> Result { + let delegator_prefix = bid_addr.delegators_prefix()?; + let delegator_keys = self.get_keys_by_prefix(&delegator_prefix).map_err(|err| { + error!("RuntimeProvider::used_reservation_count {:?}", err); + Error::Storage + })?; + let delegator_account_hashes: Vec = delegator_keys + .into_iter() + .filter_map(|key| { + if let Key::BidAddr(BidAddr::Delegator { delegator, .. }) = key { + Some(delegator) + } else { + None + } + }) + .collect(); + + let reservation_prefix = bid_addr.reservation_prefix()?; + let reservation_keys = self + .get_keys_by_prefix(&reservation_prefix) + .map_err(|err| { + error!("RuntimeProvider::delegator_count {:?}", err); + Error::Storage + })?; + + let used_reservations_count = reservation_keys + .iter() + .filter(|reservation| { + if let Key::BidAddr(BidAddr::Reservation { delegator, .. }) = reservation { + delegator_account_hashes.contains(delegator) + } else { + false + } + }) + .count(); + Ok(used_reservations_count) + } + fn vesting_schedule_period_millis(&self) -> u64 { self.vesting_schedule_period_millis() } diff --git a/storage/src/system/auction/detail.rs b/storage/src/system/auction/detail.rs index a4978fce74..ff652a25a7 100644 --- a/storage/src/system/auction/detail.rs +++ b/storage/src/system/auction/detail.rs @@ -6,10 +6,11 @@ use casper_types::{ account::AccountHash, bytesrepr::{FromBytes, ToBytes}, system::auction::{ - BidAddr, BidKind, Delegator, DelegatorBids, Error, SeigniorageAllocation, - SeigniorageRecipient, SeigniorageRecipients, SeigniorageRecipientsSnapshot, UnbondingPurse, - UnbondingPurses, ValidatorBid, ValidatorBids, ValidatorCredit, ValidatorCredits, - AUCTION_DELAY_KEY, ERA_END_TIMESTAMP_MILLIS_KEY, ERA_ID_KEY, + BidAddr, BidKind, Delegator, DelegatorBids, Error, Reservation, Reservations, + SeigniorageAllocation, SeigniorageRecipientV2, SeigniorageRecipientsSnapshotV1, + SeigniorageRecipientsSnapshotV2, SeigniorageRecipientsV2, UnbondingPurse, UnbondingPurses, + ValidatorBid, ValidatorBids, ValidatorCredit, ValidatorCredits, AUCTION_DELAY_KEY, + DELEGATION_RATE_DENOMINATOR, ERA_END_TIMESTAMP_MILLIS_KEY, ERA_ID_KEY, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, UNBONDING_DELAY_KEY, VALIDATOR_SLOTS_KEY, }, ApiError, CLTyped, EraId, Key, KeyTag, PublicKey, URef, U512, @@ -57,6 +58,7 @@ pub struct ValidatorBidsDetail { validator_bids: ValidatorBids, validator_credits: ValidatorCredits, delegator_bids: DelegatorBids, + reservations: Reservations, } impl ValidatorBidsDetail { @@ -66,6 +68,7 @@ impl ValidatorBidsDetail { validator_bids: BTreeMap::new(), validator_credits: BTreeMap::new(), delegator_bids: BTreeMap::new(), + reservations: BTreeMap::new(), } } @@ -75,8 +78,10 @@ impl ValidatorBidsDetail { validator: PublicKey, validator_bid: Box, delegators: Vec>, + reservations: Vec>, ) -> Option> { self.delegator_bids.insert(validator.clone(), delegators); + self.reservations.insert(validator.clone(), reservations); self.validator_bids.insert(validator, validator_bid) } @@ -186,11 +191,12 @@ impl ValidatorBidsDetail { } /// Consume self into in underlying collections. - pub fn destructure(self) -> (ValidatorBids, ValidatorCredits, DelegatorBids) { + pub fn destructure(self) -> (ValidatorBids, ValidatorCredits, DelegatorBids, Reservations) { ( self.validator_bids, self.validator_credits, self.delegator_bids, + self.reservations, ) } } @@ -226,7 +232,13 @@ where Some(BidKind::Validator(validator_bid)) => { let validator_public_key = validator_bid.validator_public_key(); let delegator_bids = delegators(provider, validator_public_key)?; - ret.insert_bid(validator_public_key.clone(), validator_bid, delegator_bids); + let reservations = reservations(provider, validator_public_key)?; + ret.insert_bid( + validator_public_key.clone(), + validator_bid, + delegator_bids, + reservations, + ); } Some(BidKind::Credit(credit)) => { ret.insert_credit(credit.validator_public_key().clone(), era_id, credit); @@ -327,20 +339,30 @@ where ) } -/// Returns seigniorage recipients snapshot. +/// Returns seigniorage recipients snapshot. pub fn get_seigniorage_recipients_snapshot

( provider: &mut P, -) -> Result +) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + read_from(provider, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY) +} + +/// Returns seigniorage recipients snapshot in legacy format. +pub fn get_legacy_seigniorage_recipients_snapshot

( + provider: &mut P, +) -> Result where P: StorageProvider + RuntimeProvider + ?Sized, { read_from(provider, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY) } -/// Set seigniorage recipients snapshot. +/// Sets the setigniorage recipients snapshot. pub fn set_seigniorage_recipients_snapshot

( provider: &mut P, - snapshot: SeigniorageRecipientsSnapshot, + snapshot: SeigniorageRecipientsSnapshotV2, ) -> Result<(), Error> where P: StorageProvider + RuntimeProvider + ?Sized, @@ -713,6 +735,27 @@ where } } +/// Checks if a reservation for a given delegator exists. +fn has_reservation

( + provider: &mut P, + delegator: &PublicKey, + validator: &PublicKey, +) -> Result +where + P: RuntimeProvider + StorageProvider + ?Sized, +{ + let reservation_bid_key = BidAddr::Reservation { + validator: AccountHash::from(validator), + delegator: AccountHash::from(delegator), + } + .into(); + if let Some(BidKind::Reservation(_)) = provider.read_bid(&reservation_bid_key)? { + Ok(true) + } else { + Ok(false) + } +} + /// If specified validator exists, and if validator is not yet at max delegators count, processes /// delegation. For a new delegation a delegator bid record will be created to track the delegation, /// otherwise the existing tracking record will be updated. @@ -752,13 +795,20 @@ where delegator_bid.increase_stake(amount)?; (*delegator_bid.bonding_purse(), delegator_bid) } else { - // is this validator over the delegator limit? + // is this validator over the delegator limit + // or is there a reservation for given delegator public key? let delegator_count = provider.delegator_count(&validator_bid_addr)?; - if delegator_count >= max_delegators_per_validator as usize { + let reserved_slots_count = validator_bid.reserved_slots(); + let reservation_count = provider.reservation_count(&validator_bid_addr)?; + let has_reservation = + has_reservation(provider, &delegator_public_key, &validator_public_key)?; + if delegator_count >= (max_delegators_per_validator - reserved_slots_count) as usize + && !has_reservation + { warn!( - %delegator_count, %max_delegators_per_validator, - "delegator_count {}, max_delegators_per_validator {}", - delegator_count, max_delegators_per_validator + %delegator_count, %max_delegators_per_validator, %reservation_count, %has_reservation, + "delegator_count {}, max_delegators_per_validator {}, reservation_count {}, has_reservation {}", + delegator_count, max_delegators_per_validator, reservation_count, has_reservation ); return Err(Error::ExceededDelegatorSizeLimit.into()); } @@ -796,6 +846,98 @@ where Ok(updated_amount) } +/// If specified validator exists, and if validator is not yet at max reservations count, processes +/// reservation. For a new reservation a bid record will be created to track the reservation, +/// otherwise the existing tracking record will be updated. +#[allow(clippy::too_many_arguments)] +pub fn handle_add_reservation

(provider: &mut P, reservation: Reservation) -> Result<(), Error> +where + P: StorageProvider + MintProvider + RuntimeProvider, +{ + // is there such a validator? + let validator_bid_addr = BidAddr::from(reservation.validator_public_key().clone()); + let bid = read_validator_bid(provider, &validator_bid_addr.into())?; + + // is there already a record for this delegator? + let reservation_bid_key = BidAddr::Reservation { + validator: AccountHash::from(reservation.validator_public_key()), + delegator: AccountHash::from(reservation.delegator_public_key()), + } + .into(); + if provider.read_bid(&reservation_bid_key)?.is_none() { + // ensure reservation list has capacity to create a new reservation + let reservation_count = provider.reservation_count(&validator_bid_addr)?; + let reserved_slots = bid.reserved_slots() as usize; + if reservation_count >= reserved_slots { + warn!( + %reservation_count, %reserved_slots, + "reservation_count {}, reserved_slots {}", + reservation_count, reserved_slots + ); + return Err(Error::ExceededReservationsLimit); + } + }; + + // validate specified delegation rate + if reservation.delegation_rate() > &DELEGATION_RATE_DENOMINATOR { + return Err(Error::DelegationRateTooLarge); + } + + provider.write_bid( + reservation_bid_key, + BidKind::Reservation(Box::new(reservation)), + )?; + + Ok(()) +} + +/// Attempts to remove a reservation if one exists. If not it returns an error. +/// +/// If there is already a delegator bid associated with a given reservation it validates that +/// there are free public slots available. If not, it returns an error since the delegator +/// cannot be "downgraded". +pub fn handle_cancel_reservation

( + provider: &mut P, + validator: PublicKey, + delegator: PublicKey, + max_delegators_per_validator: u32, +) -> Result<(), Error> +where + P: StorageProvider + MintProvider + RuntimeProvider, +{ + // is there such a validator? + let validator_bid_addr = BidAddr::from(validator.clone()); + let validator_bid = read_validator_bid(provider, &validator_bid_addr.into())?; + + // is there a reservation for this delegator? + let reservation_bid_addr = BidAddr::Reservation { + validator: AccountHash::from(&validator), + delegator: AccountHash::from(&delegator), + }; + if provider.read_bid(&reservation_bid_addr.into())?.is_none() { + return Err(Error::ReservationNotFound); + } + + // is there such a delegator? + let delegator_bid_addr = BidAddr::new_from_public_keys(&validator, Some(&delegator)); + if read_delegator_bid(provider, &delegator_bid_addr.into()).is_ok() { + // is there a free public slot + let reserved_slots = validator_bid.reserved_slots(); + let delegator_count = provider.delegator_count(&validator_bid_addr)?; + let used_reservation_count = provider.used_reservation_count(&validator_bid_addr)?; + let normal_delegators = delegator_count - used_reservation_count; + let public_slots = max_delegators_per_validator - reserved_slots; + + // cannot "downgrade" a delegator if there are no free public slots available + if public_slots == normal_delegators as u32 { + return Err(Error::ExceededDelegatorSizeLimit); + } + } + + provider.prune_bid(reservation_bid_addr); + Ok(()) +} + /// Returns validator bid by key. pub fn read_validator_bid

(provider: &mut P, bid_key: &Key) -> Result, Error> where @@ -879,13 +1021,52 @@ where } } +/// Returns all delegator slot reservations for given validator. +pub fn read_reservation_bids

( + provider: &mut P, + validator_public_key: &PublicKey, +) -> Result, Error> +where + P: RuntimeProvider + StorageProvider + ?Sized, +{ + let mut ret = vec![]; + let bid_addr = BidAddr::from(validator_public_key.clone()); + let reservation_bid_keys = provider.get_keys_by_prefix( + &bid_addr + .reservation_prefix() + .map_err(|_| Error::Serialization)?, + )?; + for reservation_bid_key in reservation_bid_keys { + let reservation_bid = read_reservation_bid(provider, &reservation_bid_key)?; + ret.push(*reservation_bid); + } + + Ok(ret) +} + +/// Returns delegator slot reservation bid by key. +pub fn read_reservation_bid

(provider: &mut P, bid_key: &Key) -> Result, Error> +where + P: RuntimeProvider + ?Sized + StorageProvider, +{ + if !bid_key.is_bid_addr_key() { + return Err(Error::InvalidKeyVariant); + } + if let Some(BidKind::Reservation(reservation_bid)) = provider.read_bid(bid_key)? { + Ok(reservation_bid) + } else { + Err(Error::ReservationNotFound) + } +} + /// Applies seigniorage recipient changes. pub fn seigniorage_recipients( validator_weights: &ValidatorWeights, validator_bids: &ValidatorBids, delegator_bids: &DelegatorBids, -) -> Result { - let mut recipients = SeigniorageRecipients::new(); + reservations: &Reservations, +) -> Result { + let mut recipients = SeigniorageRecipientsV2::new(); for (validator_public_key, validator_total_weight) in validator_weights { // check if validator bid exists before processing. let validator_bid = validator_bids @@ -893,7 +1074,7 @@ pub fn seigniorage_recipients( .ok_or(Error::ValidatorNotFound)?; // calculate delegator portion(s), if any let mut delegators_weight = U512::zero(); - let mut delegators_stake: BTreeMap = BTreeMap::new(); + let mut delegators_stake = BTreeMap::new(); if let Some(delegators) = delegator_bids.get(validator_public_key) { for delegator_bid in delegators { if delegator_bid.staked_amount().is_zero() { @@ -908,12 +1089,23 @@ pub fn seigniorage_recipients( } } + let mut reservation_delegation_rates = BTreeMap::new(); + if let Some(reservations) = reservations.get(validator_public_key) { + for reservation in reservations { + reservation_delegation_rates.insert( + reservation.delegator_public_key().clone(), + *reservation.delegation_rate(), + ); + } + } + // determine validator's personal stake (total weight - sum of delegators weight) let validator_stake = validator_total_weight.saturating_sub(delegators_weight); - let seigniorage_recipient = SeigniorageRecipient::new( + let seigniorage_recipient = SeigniorageRecipientV2::new( validator_stake, *validator_bid.delegation_rate(), delegators_stake, + reservation_delegation_rates, ); recipients.insert(validator_public_key.clone(), seigniorage_recipient); } @@ -924,7 +1116,23 @@ pub fn seigniorage_recipients( /// /// This is `pub` as it is used not just in the relevant auction entry point, but also by the /// engine state while directly querying for the era validators. -pub fn era_validators_from_snapshot(snapshot: SeigniorageRecipientsSnapshot) -> EraValidators { +pub fn era_validators_from_snapshot(snapshot: SeigniorageRecipientsSnapshotV2) -> EraValidators { + snapshot + .into_iter() + .map(|(era_id, recipients)| { + let validator_weights = recipients + .into_iter() + .filter_map(|(public_key, bid)| bid.total_stake().map(|stake| (public_key, stake))) + .collect::(); + (era_id, validator_weights) + }) + .collect() +} + +/// Returns the era validators from a legacy snapshot. +pub(crate) fn era_validators_from_legacy_snapshot( + snapshot: SeigniorageRecipientsSnapshotV1, +) -> EraValidators { snapshot .into_iter() .map(|(era_id, recipients)| { @@ -1013,3 +1221,27 @@ where Ok(ret) } + +/// Returns all delegator slot reservations for given validator. +pub fn reservations

( + provider: &mut P, + validator_public_key: &PublicKey, +) -> Result>, Error> +where + P: RuntimeProvider + ?Sized + StorageProvider, +{ + let mut ret = vec![]; + let bid_addr = BidAddr::from(validator_public_key.clone()); + let reservation_bid_keys = provider.get_keys_by_prefix( + &bid_addr + .reservation_prefix() + .map_err(|_| Error::Serialization)?, + )?; + + for reservation_bid_key in reservation_bid_keys { + let reservation = read_reservation_bid(provider, &reservation_bid_key)?; + ret.push(reservation); + } + + Ok(ret) +} diff --git a/storage/src/system/auction/detail.rs.orig b/storage/src/system/auction/detail.rs.orig new file mode 100644 index 0000000000..a9ed98b849 --- /dev/null +++ b/storage/src/system/auction/detail.rs.orig @@ -0,0 +1,1249 @@ +use std::{collections::BTreeMap, convert::TryInto, ops::Mul}; + +use num_rational::Ratio; + +use casper_types::{ + account::AccountHash, + bytesrepr::{FromBytes, ToBytes}, + system::auction::{ + BidAddr, BidKind, Delegator, DelegatorBids, Error, Reservation, Reservations, + SeigniorageAllocation, SeigniorageRecipientV2, SeigniorageRecipientsSnapshotV1, + SeigniorageRecipientsSnapshotV2, SeigniorageRecipientsV2, UnbondingPurse, UnbondingPurses, + ValidatorBid, ValidatorBids, ValidatorCredit, ValidatorCredits, AUCTION_DELAY_KEY, + DELEGATION_RATE_DENOMINATOR, ERA_END_TIMESTAMP_MILLIS_KEY, ERA_ID_KEY, + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, UNBONDING_DELAY_KEY, VALIDATOR_SLOTS_KEY, + }, + ApiError, CLTyped, EraId, Key, KeyTag, PublicKey, URef, U512, +}; +use tracing::{debug, error, warn}; + +use super::{ + Auction, EraValidators, MintProvider, RuntimeProvider, StorageProvider, ValidatorWeights, +}; + +/// Maximum length of bridge records chain. +/// Used when looking for the most recent bid record to avoid unbounded computations. +const MAX_BRIDGE_CHAIN_LENGTH: u64 = 20; + +fn read_from(provider: &mut P, name: &str) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, + T: FromBytes + CLTyped, +{ + let key = match provider.named_keys_get(name) { + None => { + error!("auction missing named key {:?}", name); + return Err(Error::MissingKey); + } + Some(key) => key, + }; + let uref = key.into_uref().ok_or(Error::InvalidKeyVariant)?; + let value: T = provider.read(uref)?.ok_or(Error::MissingValue)?; + Ok(value) +} + +fn write_to(provider: &mut P, name: &str, value: T) -> Result<(), Error> +where + P: StorageProvider + RuntimeProvider + ?Sized, + T: ToBytes + CLTyped, +{ + let key = provider.named_keys_get(name).ok_or(Error::MissingKey)?; + let uref = key.into_uref().ok_or(Error::InvalidKeyVariant)?; + provider.write(uref, value) +} + +/// Aggregated bid data for a Validator. +#[derive(Debug, Default)] +pub struct ValidatorBidsDetail { + validator_bids: ValidatorBids, + validator_credits: ValidatorCredits, + delegator_bids: DelegatorBids, + reservations: Reservations, +} + +impl ValidatorBidsDetail { + /// Ctor. + pub fn new() -> Self { + ValidatorBidsDetail { + validator_bids: BTreeMap::new(), + validator_credits: BTreeMap::new(), + delegator_bids: BTreeMap::new(), + reservations: BTreeMap::new(), + } + } + + /// Inserts a validator bid. + pub fn insert_bid( + &mut self, + validator: PublicKey, + validator_bid: Box, + delegators: Vec>, + reservations: Vec>, + ) -> Option> { + self.delegator_bids.insert(validator.clone(), delegators); + self.reservations.insert(validator.clone(), reservations); + self.validator_bids.insert(validator, validator_bid) + } + + /// Inserts a validator credit. + pub fn insert_credit( + &mut self, + validator: PublicKey, + era_id: EraId, + validator_credit: Box, + ) { + let credits = &mut self.validator_credits; + + credits + .entry(validator.clone()) + .and_modify(|inner| { + inner + .entry(era_id) + .and_modify(|_| { + warn!( + ?validator, + ?era_id, + "multiple validator credit entries in same era" + ) + }) + .or_insert(validator_credit.clone()); + }) + .or_insert_with(|| { + let mut inner = BTreeMap::new(); + inner.insert(era_id, validator_credit); + inner + }); + } + + /// Get validator weights. + #[allow(clippy::too_many_arguments)] + pub fn validator_weights( + &mut self, + era_ending: EraId, + era_end_timestamp_millis: u64, + vesting_schedule_period_millis: u64, + locked: bool, + include_credits: bool, + cap: Ratio, + ) -> Result { + let mut ret = BTreeMap::new(); + + for (validator_public_key, bid) in self.validator_bids.iter().filter(|(_, v)| { + locked + == v.is_locked_with_vesting_schedule( + era_end_timestamp_millis, + vesting_schedule_period_millis, + ) + && !v.inactive() + }) { + let mut staked_amount = bid.staked_amount(); + if let Some(delegators) = self.delegator_bids.get(validator_public_key) { + staked_amount = staked_amount + .checked_add(delegators.iter().map(|d| d.staked_amount()).sum()) + .ok_or(Error::InvalidAmount)?; + } + + let credit_amount = self.credit_amount( + validator_public_key, + era_ending, + staked_amount, + include_credits, + cap, + ); + let total = staked_amount.saturating_add(credit_amount); + ret.insert(validator_public_key.clone(), total); + } + + Ok(ret) + } + + fn credit_amount( + &self, + validator_public_key: &PublicKey, + era_ending: EraId, + staked_amount: U512, + include_credit: bool, + cap: Ratio, + ) -> U512 { + if !include_credit { + return U512::zero(); + } + + if let Some(inner) = self.validator_credits.get(validator_public_key) { + if let Some(credit) = inner.get(&era_ending) { + let capped = Ratio::new_raw(staked_amount, U512::one()) + .mul(cap) + .to_integer(); + let credit_amount = credit.amount(); + return credit_amount.min(capped); + } + } + + U512::zero() + } + + pub(crate) fn validator_bids(&self) -> &ValidatorBids { + &self.validator_bids + } + + pub(crate) fn validator_bids_mut(&mut self) -> &mut ValidatorBids { + &mut self.validator_bids + } + + /// Consume self into in underlying collections. + pub fn destructure(self) -> (ValidatorBids, ValidatorCredits, DelegatorBids, Reservations) { + ( + self.validator_bids, + self.validator_credits, + self.delegator_bids, + self.reservations, + ) + } +} + +/// Prunes away all validator credits for the imputed era, which should be the era ending. +/// +/// This is intended to be called at the end of an era, after calculating validator weights. +pub fn prune_validator_credits

( + provider: &mut P, + era_ending: EraId, + validator_credits: &ValidatorCredits, +) where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + for (validator_public_key, inner) in validator_credits { + if inner.contains_key(&era_ending) { + provider.prune_bid(BidAddr::new_credit(validator_public_key, era_ending)) + } + } +} + +/// Returns the imputed validator bids. +pub fn get_validator_bids

(provider: &mut P, era_id: EraId) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + let bids_keys = provider.get_keys(&KeyTag::BidAddr)?; + + let mut ret = ValidatorBidsDetail::new(); + + for key in bids_keys { + match provider.read_bid(&key)? { + Some(BidKind::Validator(validator_bid)) => { + let validator_public_key = validator_bid.validator_public_key(); + let delegator_bids = delegators(provider, validator_public_key)?; + let reservations = reservations(provider, validator_public_key)?; + ret.insert_bid( + validator_public_key.clone(), + validator_bid, + delegator_bids, + reservations, + ); + } + Some(BidKind::Credit(credit)) => { + ret.insert_credit(credit.validator_public_key().clone(), era_id, credit); + } + Some(_) => { + // noop + } + None => return Err(Error::ValidatorNotFound), + }; + } + + Ok(ret) +} + +/// Sets the imputed validator bids. +pub fn set_validator_bids

(provider: &mut P, validators: ValidatorBids) -> Result<(), Error> +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + for (validator_public_key, validator_bid) in validators.into_iter() { + let bid_addr = BidAddr::from(validator_public_key.clone()); + provider.write_bid(bid_addr.into(), BidKind::Validator(validator_bid))?; + } + Ok(()) +} + +/// Returns the unbonding purses. +pub fn get_unbonding_purses

(provider: &mut P) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + let unbond_keys = provider.get_keys(&KeyTag::Unbond)?; + + let mut ret = BTreeMap::new(); + + for key in unbond_keys { + let account_hash = match key { + Key::Unbond(account_hash) => account_hash, + _ => return Err(Error::InvalidKeyVariant), + }; + let unbonding_purses = provider.read_unbonds(&account_hash)?; + ret.insert(account_hash, unbonding_purses); + } + + Ok(ret) +} + +/// Sets the unbonding purses. +pub fn set_unbonding_purses

( + provider: &mut P, + unbonding_purses: UnbondingPurses, +) -> Result<(), Error> +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + for (account_hash, unbonding_purses) in unbonding_purses.into_iter() { + provider.write_unbonds(account_hash, unbonding_purses)?; + } + Ok(()) +} + +/// Returns the era id. +pub fn get_era_id

(provider: &mut P) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + read_from(provider, ERA_ID_KEY) +} + +/// Sets the era id. +pub fn set_era_id

(provider: &mut P, era_id: EraId) -> Result<(), Error> +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + write_to(provider, ERA_ID_KEY, era_id) +} + +/// Returns the era end timestamp. +pub fn get_era_end_timestamp_millis

(provider: &mut P) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + read_from(provider, ERA_END_TIMESTAMP_MILLIS_KEY) +} + +/// Sets the era end timestamp. +pub fn set_era_end_timestamp_millis

( + provider: &mut P, + era_end_timestamp_millis: u64, +) -> Result<(), Error> +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + write_to( + provider, + ERA_END_TIMESTAMP_MILLIS_KEY, + era_end_timestamp_millis, + ) +} + +/// Returns seigniorage recipients snapshot. +pub fn get_seigniorage_recipients_snapshot

( + provider: &mut P, +) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + read_from(provider, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY) +} + +pub fn get_legacy_seigniorage_recipients_snapshot

( + provider: &mut P, +) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + read_from(provider, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY) +} + +/// Set seigniorage recipients snapshot. +pub fn set_seigniorage_recipients_snapshot

( + provider: &mut P, + snapshot: SeigniorageRecipientsSnapshotV2, +) -> Result<(), Error> +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + write_to(provider, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, snapshot) +} + +/// Returns the number of validator slots. +pub fn get_validator_slots

(provider: &mut P) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + let validator_slots: u32 = match read_from(provider, VALIDATOR_SLOTS_KEY) { + Ok(ret) => ret, + Err(err) => { + error!("Failed to find VALIDATOR_SLOTS_KEY {}", err); + return Err(err); + } + }; + let validator_slots = validator_slots + .try_into() + .map_err(|_| Error::InvalidValidatorSlotsValue)?; + Ok(validator_slots) +} + +/// Returns auction delay. +pub fn get_auction_delay

(provider: &mut P) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + let auction_delay: u64 = match read_from(provider, AUCTION_DELAY_KEY) { + Ok(ret) => ret, + Err(err) => { + error!("Failed to find AUCTION_DELAY_KEY {}", err); + return Err(err); + } + }; + Ok(auction_delay) +} + +fn get_unbonding_delay

(provider: &mut P) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + read_from(provider, UNBONDING_DELAY_KEY) +} + +/// Iterates over unbonding entries and checks if a locked amount can be paid already if +/// a specific era is reached. +/// +/// This function can be called by the system only. +pub fn process_unbond_requests( + provider: &mut P, + max_delegators_per_validator: u32, +) -> Result<(), ApiError> { + if provider.get_caller() != PublicKey::System.to_account_hash() { + return Err(Error::InvalidCaller.into()); + } + + // Update `unbonding_purses` data + let mut unbonding_purses: UnbondingPurses = get_unbonding_purses(provider)?; + + let current_era_id = provider.read_era_id()?; + + let unbonding_delay = get_unbonding_delay(provider)?; + + for unbonding_list in unbonding_purses.values_mut() { + let mut new_unbonding_list = Vec::new(); + + for unbonding_purse in unbonding_list.iter() { + // Since `process_unbond_requests` is run before `run_auction`, we should check if + // current era id + unbonding delay is equal or greater than the `era_of_creation` that + // was calculated on `unbond` attempt. + if current_era_id >= unbonding_purse.era_of_creation() + unbonding_delay { + let redelegation_result = + handle_redelegation(provider, unbonding_purse, max_delegators_per_validator) + .map_err(|err| { + error!(?err, ?unbonding_purse, "error processing unbond"); + err + })?; + match redelegation_result { + UnbondRedelegationOutcome::SuccessfullyRedelegated => { + // noop; on successful redelegation, no actual unbond occurs + } + uro @ UnbondRedelegationOutcome::NonexistantRedelegationTarget + | uro @ UnbondRedelegationOutcome::DelegationAmountBelowCap + | uro @ UnbondRedelegationOutcome::DelegationAmountAboveCap + | uro @ UnbondRedelegationOutcome::RedelegationTargetHasNoVacancy + | uro @ UnbondRedelegationOutcome::RedelegationTargetIsUnstaked + | uro @ UnbondRedelegationOutcome::Withdrawal => { + // Move funds from bid purse to unbonding purse + provider.unbond(unbonding_purse).map_err(|err| { + error!(?err, ?uro, "error unbonding purse"); + ApiError::from(Error::TransferToUnbondingPurse) + })? + } + } + } else { + new_unbonding_list.push(unbonding_purse.clone()); + } + } + *unbonding_list = new_unbonding_list; + } + + set_unbonding_purses(provider, unbonding_purses)?; + Ok(()) +} + +/// Creates a new purse in unbonding_purses given a validator's key, amount, and a destination +/// unbonding purse. Returns the amount of motes remaining in the validator's bid purse. +pub fn create_unbonding_purse( + provider: &mut P, + validator_public_key: PublicKey, + unbonder_public_key: PublicKey, + bonding_purse: URef, + amount: U512, + new_validator: Option, +) -> Result<(), Error> { + if provider + .available_balance(bonding_purse)? + .unwrap_or_default() + < amount + { + return Err(Error::UnbondTooLarge); + } + + let account_hash = AccountHash::from(&unbonder_public_key); + let mut unbonding_purses = provider.read_unbonds(&account_hash)?; + let era_of_creation = provider.read_era_id()?; + let new_unbonding_purse = UnbondingPurse::new( + bonding_purse, + validator_public_key, + unbonder_public_key, + era_of_creation, + amount, + new_validator, + ); + unbonding_purses.push(new_unbonding_purse); + provider.write_unbonds(account_hash, unbonding_purses)?; + + Ok(()) +} + +/// Returns most recent validator public key if public key has been changed +/// or the validator has withdrawn their bid completely. +pub fn get_most_recent_validator_public_key

( + provider: &mut P, + mut validator_public_key: PublicKey, +) -> Result +where + P: RuntimeProvider + StorageProvider, +{ + let mut validator_bid_addr = BidAddr::from(validator_public_key.clone()); + let mut found_validator_bid_chain_tip = false; + for _ in 0..MAX_BRIDGE_CHAIN_LENGTH { + match provider.read_bid(&validator_bid_addr.into())? { + Some(BidKind::Validator(validator_bid)) => { + validator_public_key = validator_bid.validator_public_key().clone(); + found_validator_bid_chain_tip = true; + break; + } + Some(BidKind::Bridge(bridge)) => { + validator_public_key = bridge.new_validator_public_key().clone(); + validator_bid_addr = BidAddr::from(validator_public_key.clone()); + } + _ => { + // Validator has withdrawn their bid, so there's nothing at the tip. + // In this case we add the reward to a delegator's unbond. + found_validator_bid_chain_tip = true; + break; + } + }; + } + if !found_validator_bid_chain_tip { + Err(Error::BridgeRecordChainTooLong) + } else { + Ok(validator_public_key) + } +} + +/// Attempts to apply the delegator reward to the existing stake. If the reward recipient has +/// completely unstaked, applies it to their unbond instead. In either case, returns +/// the purse the amount should be applied to. +pub fn distribute_delegator_rewards

( + provider: &mut P, + seigniorage_allocations: &mut Vec, + validator_public_key: PublicKey, + rewards: impl IntoIterator, +) -> Result, Error> +where + P: RuntimeProvider + StorageProvider, +{ + let mut delegator_payouts = Vec::new(); + for (delegator_public_key, delegator_reward_trunc) in rewards { + let bid_key = + BidAddr::new_from_public_keys(&validator_public_key, Some(&delegator_public_key)) + .into(); + + let delegator_bonding_purse = match read_delegator_bid(provider, &bid_key) { + Ok(mut delegator_bid) if !delegator_bid.staked_amount().is_zero() => { + let purse = *delegator_bid.bonding_purse(); + delegator_bid.increase_stake(delegator_reward_trunc)?; + provider.write_bid(bid_key, BidKind::Delegator(delegator_bid))?; + purse + } + Ok(_) | Err(Error::DelegatorNotFound) => { + // check to see if there are unbond entries for this recipient + // (validator + delegator match), and if there are apply the amount + // to the unbond entry with the highest era. + let account_hash = delegator_public_key.to_account_hash(); + match provider.read_unbonds(&account_hash) { + Ok(mut unbonds) => { + match unbonds + .iter_mut() + .filter(|x| x.validator_public_key() == &validator_public_key) + .max_by(|x, y| x.era_of_creation().cmp(&y.era_of_creation())) + { + Some(unbond) => { + let purse = *unbond.bonding_purse(); + let new_amount = + unbond.amount().saturating_add(delegator_reward_trunc); + unbond.with_amount(new_amount); + provider.write_unbonds(account_hash, unbonds)?; + purse + } + None => { + return Err(Error::DelegatorNotFound); + } + } + } + Err(err) => return Err(err), + } + } + Err(err) => { + return Err(err); + } + }; + + delegator_payouts.push(( + delegator_public_key.to_account_hash(), + delegator_reward_trunc, + delegator_bonding_purse, + )); + + let allocation = SeigniorageAllocation::delegator( + delegator_public_key, + validator_public_key.clone(), + delegator_reward_trunc, + ); + + seigniorage_allocations.push(allocation); + } + + Ok(delegator_payouts) +} + +/// Attempts to apply the validator reward to the existing stake. If the reward recipient has +/// completely unstaked, applies it to their unbond instead. In either case, returns +/// the purse the amount should be applied to. +pub fn distribute_validator_rewards

( + provider: &mut P, + seigniorage_allocations: &mut Vec, + validator_public_key: PublicKey, + amount: U512, +) -> Result +where + P: StorageProvider, +{ + let bid_key: Key = BidAddr::from(validator_public_key.clone()).into(); + let bonding_purse = match read_current_validator_bid(provider, bid_key) { + Ok(mut validator_bid) => { + let purse = *validator_bid.bonding_purse(); + validator_bid.increase_stake(amount)?; + provider.write_bid(bid_key, BidKind::Validator(validator_bid))?; + purse + } + Err(Error::ValidatorNotFound) => { + // check to see if there are unbond entries for this recipient, and if there are + // apply the amount to the unbond entry with the highest era. + let account_hash = validator_public_key.to_account_hash(); + match provider.read_unbonds(&account_hash) { + Ok(mut unbonds) => { + match unbonds + .iter_mut() + .max_by(|x, y| x.era_of_creation().cmp(&y.era_of_creation())) + { + Some(unbond) => { + let purse = *unbond.bonding_purse(); + let new_amount = unbond.amount().saturating_add(amount); + unbond.with_amount(new_amount); + provider.write_unbonds(account_hash, unbonds)?; + purse + } + None => { + return Err(Error::ValidatorNotFound); + } + } + } + Err(err) => return Err(err), + } + } + Err(err) => return Err(err), + }; + + let allocation = SeigniorageAllocation::validator(validator_public_key, amount); + seigniorage_allocations.push(allocation); + Ok(bonding_purse) +} + +#[derive(Debug)] +enum UnbondRedelegationOutcome { + Withdrawal, + SuccessfullyRedelegated, + NonexistantRedelegationTarget, + RedelegationTargetHasNoVacancy, + RedelegationTargetIsUnstaked, + DelegationAmountBelowCap, + DelegationAmountAboveCap, +} + +fn handle_redelegation

( + provider: &mut P, + unbonding_purse: &UnbondingPurse, + max_delegators_per_validator: u32, +) -> Result +where + P: StorageProvider + MintProvider + RuntimeProvider, +{ + let redelegation_target_public_key = match unbonding_purse.new_validator() { + Some(public_key) => { + // get updated key if `ValidatorBid` public key was changed + let validator_bid_addr = BidAddr::from(public_key.clone()); + match read_current_validator_bid(provider, validator_bid_addr.into()) { + Ok(validator_bid) => validator_bid.validator_public_key().clone(), + Err(err) => { + error!(?err, ?unbonding_purse, redelegate_to=?public_key, "error redelegating"); + return Ok(UnbondRedelegationOutcome::NonexistantRedelegationTarget); + } + } + } + None => return Ok(UnbondRedelegationOutcome::Withdrawal), + }; + + let redelegation = handle_delegation( + provider, + unbonding_purse.unbonder_public_key().clone(), + redelegation_target_public_key, + *unbonding_purse.bonding_purse(), + *unbonding_purse.amount(), + max_delegators_per_validator, + ); + match redelegation { + Ok(_) => Ok(UnbondRedelegationOutcome::SuccessfullyRedelegated), + Err(ApiError::AuctionError(err)) if err == Error::BondTooSmall as u8 => { + Ok(UnbondRedelegationOutcome::RedelegationTargetIsUnstaked) + } + Err(ApiError::AuctionError(err)) if err == Error::DelegationAmountTooSmall as u8 => { + Ok(UnbondRedelegationOutcome::DelegationAmountBelowCap) + } + Err(ApiError::AuctionError(err)) if err == Error::DelegationAmountTooLarge as u8 => { + Ok(UnbondRedelegationOutcome::DelegationAmountAboveCap) + } + Err(ApiError::AuctionError(err)) if err == Error::ValidatorNotFound as u8 => { + Ok(UnbondRedelegationOutcome::NonexistantRedelegationTarget) + } + Err(ApiError::AuctionError(err)) if err == Error::ExceededDelegatorSizeLimit as u8 => { + Ok(UnbondRedelegationOutcome::RedelegationTargetHasNoVacancy) + } + Err(err) => Err(err), + } +} + +/// Checks if a reservation for a given delegator exists. +fn has_reservation

( + provider: &mut P, + delegator: &PublicKey, + validator: &PublicKey, +) -> Result +where + P: RuntimeProvider + StorageProvider + ?Sized, +{ + let reservation_bid_key = BidAddr::Reservation { + validator: AccountHash::from(validator), + delegator: AccountHash::from(delegator), + } + .into(); + if let Some(BidKind::Reservation(_)) = provider.read_bid(&reservation_bid_key)? { + Ok(true) + } else { + Ok(false) + } +} + +/// If specified validator exists, and if validator is not yet at max delegators count, processes +/// delegation. For a new delegation a delegator bid record will be created to track the delegation, +/// otherwise the existing tracking record will be updated. +#[allow(clippy::too_many_arguments)] +pub fn handle_delegation

( + provider: &mut P, + delegator_public_key: PublicKey, + validator_public_key: PublicKey, + source: URef, + amount: U512, + max_delegators_per_validator: u32, +) -> Result +where + P: StorageProvider + MintProvider + RuntimeProvider, +{ + if amount.is_zero() { + return Err(Error::BondTooSmall.into()); + } + + let validator_bid_addr = BidAddr::from(validator_public_key.clone()); + // is there such a validator? + let validator_bid = read_validator_bid(provider, &validator_bid_addr.into())?; + if amount < U512::from(validator_bid.minimum_delegation_amount()) { + return Err(Error::DelegationAmountTooSmall.into()); + } + if amount > U512::from(validator_bid.maximum_delegation_amount()) { + return Err(Error::DelegationAmountTooLarge.into()); + } + + // is there already a record for this delegator? + let delegator_bid_key = + BidAddr::new_from_public_keys(&validator_public_key, Some(&delegator_public_key)).into(); + + let (target, delegator_bid) = if let Some(BidKind::Delegator(mut delegator_bid)) = + provider.read_bid(&delegator_bid_key)? + { + delegator_bid.increase_stake(amount)?; + (*delegator_bid.bonding_purse(), delegator_bid) + } else { + // is this validator over the delegator limit + // or is there a reservation for given delegator public key? + let delegator_count = provider.delegator_count(&validator_bid_addr)?; + let reserved_slots_count = validator_bid.reserved_slots(); + let reservation_count = provider.reservation_count(&validator_bid_addr)?; + let has_reservation = + has_reservation(provider, &delegator_public_key, &validator_public_key)?; + if delegator_count >= (max_delegators_per_validator - reserved_slots_count) as usize + && !has_reservation + { + warn!( + %delegator_count, %max_delegators_per_validator, %reservation_count, %has_reservation, + "delegator_count {}, max_delegators_per_validator {}, reservation_count {}, has_reservation {}", + delegator_count, max_delegators_per_validator, reservation_count, has_reservation + ); + return Err(Error::ExceededDelegatorSizeLimit.into()); + } + + let bonding_purse = provider.create_purse()?; + let delegator_bid = Delegator::unlocked( + delegator_public_key, + amount, + bonding_purse, + validator_public_key, + ); + (bonding_purse, Box::new(delegator_bid)) + }; + + // transfer token to bonding purse + provider + .mint_transfer_direct( + Some(PublicKey::System.to_account_hash()), + source, + target, + amount, + None, + ) + .map_err(|_| Error::TransferToDelegatorPurse)? + .map_err(|mint_error| { + // Propagate mint contract's error that occured during execution of transfer + // entrypoint. This will improve UX in case of (for example) + // unapproved spending limit error. + ApiError::from(mint_error) + })?; + + let updated_amount = delegator_bid.staked_amount(); + provider.write_bid(delegator_bid_key, BidKind::Delegator(delegator_bid))?; + + Ok(updated_amount) +} + +<<<<<<< HEAD +/// If specified validator exists, and if validator is not yet at max reservations count, processes +/// reservation. For a new reservation a bid record will be created to track the reservation, +/// otherwise the existing tracking record will be updated. +#[allow(clippy::too_many_arguments)] +pub fn handle_add_reservation

(provider: &mut P, reservation: Reservation) -> Result<(), Error> +where + P: StorageProvider + MintProvider + RuntimeProvider, +{ + // is there such a validator? + let validator_bid_addr = BidAddr::from(reservation.validator_public_key().clone()); + let bid = read_validator_bid(provider, &validator_bid_addr.into())?; + + // is there already a record for this delegator? + let reservation_bid_key = BidAddr::Reservation { + validator: AccountHash::from(reservation.validator_public_key()), + delegator: AccountHash::from(reservation.delegator_public_key()), + } + .into(); + if provider.read_bid(&reservation_bid_key)?.is_none() { + // ensure reservation list has capacity to create a new reservation + let reservation_count = provider.reservation_count(&validator_bid_addr)?; + let reserved_slots = bid.reserved_slots() as usize; + if reservation_count >= reserved_slots { + warn!( + %reservation_count, %reserved_slots, + "reservation_count {}, reserved_slots {}", + reservation_count, reserved_slots + ); + return Err(Error::ExceededReservationsLimit); + } + }; + + // validate specified delegation rate + if reservation.delegation_rate() > &DELEGATION_RATE_DENOMINATOR { + return Err(Error::DelegationRateTooLarge); + } + + provider.write_bid( + reservation_bid_key, + BidKind::Reservation(Box::new(reservation)), + )?; + + Ok(()) +} + +/// Attempts to remove a reservation if one exists. If not it returns an error. +/// +/// If there is already a delegator bid associated with a given reservation it validates that +/// there are free public slots available. If not, it returns an error since the delegator +/// cannot be "downgraded". +pub fn handle_cancel_reservation

( + provider: &mut P, + validator: PublicKey, + delegator: PublicKey, + max_delegators_per_validator: u32, +) -> Result<(), Error> +where + P: StorageProvider + MintProvider + RuntimeProvider, +{ + // is there such a validator? + let validator_bid_addr = BidAddr::from(validator.clone()); + let validator_bid = read_validator_bid(provider, &validator_bid_addr.into())?; + + // is there a reservation for this delegator? + let reservation_bid_addr = BidAddr::Reservation { + validator: AccountHash::from(&validator), + delegator: AccountHash::from(&delegator), + }; + if provider.read_bid(&reservation_bid_addr.into())?.is_none() { + return Err(Error::ReservationNotFound); + } + + // is there such a delegator? + let delegator_bid_addr = BidAddr::new_from_public_keys(&validator, Some(&delegator)); + if read_delegator_bid(provider, &delegator_bid_addr.into()).is_ok() { + // is there a free public slot + let reserved_slots = validator_bid.reserved_slots(); + let delegator_count = provider.delegator_count(&validator_bid_addr)?; + let used_reservation_count = provider.used_reservation_count(&validator_bid_addr)?; + let normal_delegators = delegator_count - used_reservation_count; + let public_slots = max_delegators_per_validator - reserved_slots; + + // cannot "downgrade" a delegator if there are no free public slots available + if public_slots == normal_delegators as u32 { + return Err(Error::ExceededDelegatorSizeLimit); + } + } + + provider.prune_bid(reservation_bid_addr); + Ok(()) +} + +======= +/// Returns validator bid by key. +>>>>>>> feat-2.0 +pub fn read_validator_bid

(provider: &mut P, bid_key: &Key) -> Result, Error> +where + P: StorageProvider + ?Sized, +{ + if !bid_key.is_bid_addr_key() { + return Err(Error::InvalidKeyVariant); + } + if let Some(BidKind::Validator(validator_bid)) = provider.read_bid(bid_key)? { + Ok(validator_bid) + } else { + Err(Error::ValidatorNotFound) + } +} + +/// Returns current `ValidatorBid` in case the public key was changed. +pub fn read_current_validator_bid

( + provider: &mut P, + mut bid_key: Key, +) -> Result, Error> +where + P: StorageProvider + ?Sized, +{ + if !bid_key.is_bid_addr_key() { + return Err(Error::InvalidKeyVariant); + } + + for _ in 0..MAX_BRIDGE_CHAIN_LENGTH { + match provider.read_bid(&bid_key)? { + Some(BidKind::Validator(validator_bid)) => return Ok(validator_bid), + Some(BidKind::Bridge(bridge)) => { + debug!( + ?bid_key, + ?bridge, + "read_current_validator_bid: bridge found" + ); + let validator_bid_addr = BidAddr::from(bridge.new_validator_public_key().clone()); + bid_key = validator_bid_addr.into(); + } + _ => break, + } + } + Err(Error::ValidatorNotFound) +} + +/// Returns all delegator bids for imputed validator. +pub fn read_delegator_bids

( + provider: &mut P, + validator_public_key: &PublicKey, +) -> Result, Error> +where + P: RuntimeProvider + StorageProvider + ?Sized, +{ + let mut ret = vec![]; + let bid_addr = BidAddr::from(validator_public_key.clone()); + let delegator_bid_keys = provider.get_keys_by_prefix( + &bid_addr + .delegators_prefix() + .map_err(|_| Error::Serialization)?, + )?; + for delegator_bid_key in delegator_bid_keys { + let delegator_bid = read_delegator_bid(provider, &delegator_bid_key)?; + ret.push(*delegator_bid); + } + + Ok(ret) +} + +/// Returns delegator bid by key. +pub fn read_delegator_bid

(provider: &mut P, bid_key: &Key) -> Result, Error> +where + P: RuntimeProvider + ?Sized + StorageProvider, +{ + if !bid_key.is_bid_addr_key() { + return Err(Error::InvalidKeyVariant); + } + if let Some(BidKind::Delegator(delegator_bid)) = provider.read_bid(bid_key)? { + Ok(delegator_bid) + } else { + Err(Error::DelegatorNotFound) + } +} + +<<<<<<< HEAD +pub fn read_reservation_bids

( + provider: &mut P, + validator_public_key: &PublicKey, +) -> Result, Error> +where + P: RuntimeProvider + StorageProvider + ?Sized, +{ + let mut ret = vec![]; + let bid_addr = BidAddr::from(validator_public_key.clone()); + let reservation_bid_keys = provider.get_keys_by_prefix( + &bid_addr + .reservation_prefix() + .map_err(|_| Error::Serialization)?, + )?; + for reservation_bid_key in reservation_bid_keys { + let reservation_bid = read_reservation_bid(provider, &reservation_bid_key)?; + ret.push(*reservation_bid); + } + + Ok(ret) +} + +pub fn read_reservation_bid

(provider: &mut P, bid_key: &Key) -> Result, Error> +where + P: RuntimeProvider + ?Sized + StorageProvider, +{ + if !bid_key.is_bid_addr_key() { + return Err(Error::InvalidKeyVariant); + } + if let Some(BidKind::Reservation(reservation_bid)) = provider.read_bid(bid_key)? { + Ok(reservation_bid) + } else { + Err(Error::ReservationNotFound) + } +} + +======= +/// Applies seigniorage recipient changes. +>>>>>>> feat-2.0 +pub fn seigniorage_recipients( + validator_weights: &ValidatorWeights, + validator_bids: &ValidatorBids, + delegator_bids: &DelegatorBids, + reservations: &Reservations, +) -> Result { + let mut recipients = SeigniorageRecipientsV2::new(); + for (validator_public_key, validator_total_weight) in validator_weights { + // check if validator bid exists before processing. + let validator_bid = validator_bids + .get(validator_public_key) + .ok_or(Error::ValidatorNotFound)?; + // calculate delegator portion(s), if any + let mut delegators_weight = U512::zero(); + let mut delegators_stake = BTreeMap::new(); + if let Some(delegators) = delegator_bids.get(validator_public_key) { + for delegator_bid in delegators { + if delegator_bid.staked_amount().is_zero() { + continue; + } + let delegator_staked_amount = delegator_bid.staked_amount(); + delegators_weight = delegators_weight.saturating_add(delegator_staked_amount); + delegators_stake.insert( + delegator_bid.delegator_public_key().clone(), + delegator_staked_amount, + ); + } + } + + let mut reservation_delegation_rates = BTreeMap::new(); + if let Some(reservations) = reservations.get(validator_public_key) { + for reservation in reservations { + reservation_delegation_rates.insert( + reservation.delegator_public_key().clone(), + *reservation.delegation_rate(), + ); + } + } + + // determine validator's personal stake (total weight - sum of delegators weight) + let validator_stake = validator_total_weight.saturating_sub(delegators_weight); + let seigniorage_recipient = SeigniorageRecipientV2::new( + validator_stake, + *validator_bid.delegation_rate(), + delegators_stake, + reservation_delegation_rates, + ); + recipients.insert(validator_public_key.clone(), seigniorage_recipient); + } + Ok(recipients) +} + +/// Returns the era validators from a snapshot. +/// +/// This is `pub` as it is used not just in the relevant auction entry point, but also by the +/// engine state while directly querying for the era validators. +pub fn era_validators_from_snapshot(snapshot: SeigniorageRecipientsSnapshotV2) -> EraValidators { + snapshot + .into_iter() + .map(|(era_id, recipients)| { + let validator_weights = recipients + .into_iter() + .filter_map(|(public_key, bid)| bid.total_stake().map(|stake| (public_key, stake))) + .collect::(); + (era_id, validator_weights) + }) + .collect() +} + +/// Returns the era validators from a legacy snapshot. +pub(crate) fn era_validators_from_legacy_snapshot( + snapshot: SeigniorageRecipientsSnapshotV1, +) -> EraValidators { + snapshot + .into_iter() + .map(|(era_id, recipients)| { + let validator_weights = recipients + .into_iter() + .filter_map(|(public_key, bid)| bid.total_stake().map(|stake| (public_key, stake))) + .collect::(); + (era_id, validator_weights) + }) + .collect() +} + +/// Initializes the vesting schedule of provided bid if the provided timestamp is greater than +/// or equal to the bid's initial release timestamp and the bid is owned by a genesis +/// validator. +/// +/// Returns `true` if the provided bid's vesting schedule was initialized. +pub fn process_with_vesting_schedule

( + provider: &mut P, + validator_bid: &mut ValidatorBid, + timestamp_millis: u64, + vesting_schedule_period_millis: u64, +) -> Result +where + P: StorageProvider + RuntimeProvider + ?Sized, +{ + let validator_public_key = validator_bid.validator_public_key().clone(); + + let delegator_bids = read_delegator_bids(provider, &validator_public_key)?; + for mut delegator_bid in delegator_bids { + let delegator_staked_amount = delegator_bid.staked_amount(); + let delegator_vesting_schedule = match delegator_bid.vesting_schedule_mut() { + Some(vesting_schedule) => vesting_schedule, + None => continue, + }; + if timestamp_millis < delegator_vesting_schedule.initial_release_timestamp_millis() { + continue; + } + if delegator_vesting_schedule + .initialize_with_schedule(delegator_staked_amount, vesting_schedule_period_millis) + { + let delegator_bid_addr = BidAddr::new_from_public_keys( + &validator_public_key, + Some(delegator_bid.delegator_public_key()), + ); + provider.write_bid( + delegator_bid_addr.into(), + BidKind::Delegator(Box::new(delegator_bid)), + )?; + } + } + + let validator_staked_amount = validator_bid.staked_amount(); + let validator_vesting_schedule = match validator_bid.vesting_schedule_mut() { + Some(vesting_schedule) => vesting_schedule, + None => return Ok(false), + }; + if timestamp_millis < validator_vesting_schedule.initial_release_timestamp_millis() { + Ok(false) + } else { + Ok(validator_vesting_schedule + .initialize_with_schedule(validator_staked_amount, vesting_schedule_period_millis)) + } +} + +/// Returns all delegators for imputed validator. +pub fn delegators

( + provider: &mut P, + validator_public_key: &PublicKey, +) -> Result>, Error> +where + P: RuntimeProvider + ?Sized + StorageProvider, +{ + let mut ret = vec![]; + let bid_addr = BidAddr::from(validator_public_key.clone()); + let delegator_bid_keys = provider.get_keys_by_prefix( + &bid_addr + .delegators_prefix() + .map_err(|_| Error::Serialization)?, + )?; + + for delegator_bid_key in delegator_bid_keys { + let delegator = read_delegator_bid(provider, &delegator_bid_key)?; + ret.push(delegator); + } + + Ok(ret) +} + +pub fn reservations

( + provider: &mut P, + validator_public_key: &PublicKey, +) -> Result>, Error> +where + P: RuntimeProvider + ?Sized + StorageProvider, +{ + let mut ret = vec![]; + let bid_addr = BidAddr::from(validator_public_key.clone()); + let reservation_bid_keys = provider.get_keys_by_prefix( + &bid_addr + .reservation_prefix() + .map_err(|_| Error::Serialization)?, + )?; + + for reservation_bid_key in reservation_bid_keys { + let reservation = read_reservation_bid(provider, &reservation_bid_key)?; + ret.push(reservation); + } + + Ok(ret) +} diff --git a/storage/src/system/auction/providers.rs b/storage/src/system/auction/providers.rs index 4bc754b526..82871bc371 100644 --- a/storage/src/system/auction/providers.rs +++ b/storage/src/system/auction/providers.rs @@ -31,6 +31,12 @@ pub trait RuntimeProvider { /// Returns the current number of delegators for this validator. fn delegator_count(&mut self, bid_addr: &BidAddr) -> Result; + /// Returns number of reservations for this validator. + fn reservation_count(&mut self, bid_addr: &BidAddr) -> Result; + + /// Returns number of reservations for which a delegator bid exists. + fn used_reservation_count(&mut self, bid_addr: &BidAddr) -> Result; + /// Returns a 32-byte BLAKE2b digest fn blake2b>(&self, data: T) -> [u8; BLAKE2B_DIGEST_LENGTH] { crypto::blake2b(data) diff --git a/storage/src/system/genesis.rs b/storage/src/system/genesis.rs index 81ad4c368c..6073c57862 100644 --- a/storage/src/system/genesis.rs +++ b/storage/src/system/genesis.rs @@ -26,11 +26,13 @@ use casper_types::{ execution::Effects, system::{ auction::{ - self, BidAddr, BidKind, DelegationRate, Delegator, SeigniorageRecipient, - SeigniorageRecipients, SeigniorageRecipientsSnapshot, Staking, ValidatorBid, - AUCTION_DELAY_KEY, DELEGATION_RATE_DENOMINATOR, ERA_END_TIMESTAMP_MILLIS_KEY, - ERA_ID_KEY, INITIAL_ERA_END_TIMESTAMP_MILLIS, INITIAL_ERA_ID, LOCKED_FUNDS_PERIOD_KEY, - SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, UNBONDING_DELAY_KEY, VALIDATOR_SLOTS_KEY, + self, BidAddr, BidKind, DelegationRate, Delegator, SeigniorageRecipientV2, + SeigniorageRecipients, SeigniorageRecipientsSnapshot, SeigniorageRecipientsSnapshotV2, + SeigniorageRecipientsV2, Staking, ValidatorBid, AUCTION_DELAY_KEY, + DEFAULT_SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION, DELEGATION_RATE_DENOMINATOR, + ERA_END_TIMESTAMP_MILLIS_KEY, ERA_ID_KEY, INITIAL_ERA_END_TIMESTAMP_MILLIS, + INITIAL_ERA_ID, LOCKED_FUNDS_PERIOD_KEY, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY, UNBONDING_DELAY_KEY, VALIDATOR_SLOTS_KEY, }, handle_payment::{self, ACCUMULATION_PURSE_KEY}, mint::{ @@ -421,6 +423,7 @@ where release_timestamp_millis, 0, u64::MAX, + 0, ); // Set up delegator entries attached to genesis validators @@ -514,11 +517,31 @@ where |_| GenesisError::CLValue(SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.to_string()), )?), ); + named_keys.insert( SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.into(), initial_seigniorage_recipients_uref.into(), ); + // initialize snapshot version flag + let initial_seigniorage_recipients_version_uref = self + .address_generator + .borrow_mut() + .new_uref(AccessRights::READ_ADD_WRITE); + self.tracking_copy.borrow_mut().write( + initial_seigniorage_recipients_version_uref.into(), + StoredValue::CLValue( + CLValue::from_t(DEFAULT_SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION).map_err(|_| { + GenesisError::CLValue(SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY.to_string()) + })?, + ), + ); + + named_keys.insert( + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY.into(), + initial_seigniorage_recipients_version_uref.into(), + ); + // store all delegator and validator bids for (validator_public_key, (validator_bid, delegators)) in staked { for (delegator_public_key, delegator_bid) in delegators { @@ -663,24 +686,25 @@ where &self, staked: &Staking, auction_delay: u64, - ) -> BTreeMap { + ) -> BTreeMap { let initial_snapshot_range = INITIAL_ERA_ID.iter_inclusive(auction_delay); - let mut seigniorage_recipients = SeigniorageRecipients::new(); + let mut seigniorage_recipients = SeigniorageRecipientsV2::new(); for (validator_public_key, (validator_bid, delegators)) in staked { let mut delegator_stake: BTreeMap = BTreeMap::new(); for (k, v) in delegators { delegator_stake.insert(k.clone(), v.staked_amount()); } - let recipient = SeigniorageRecipient::new( + let recipient = SeigniorageRecipientV2::new( validator_bid.staked_amount(), *validator_bid.delegation_rate(), delegator_stake, + BTreeMap::new(), ); seigniorage_recipients.insert(validator_public_key.clone(), recipient); } - let mut initial_seigniorage_recipients = SeigniorageRecipientsSnapshot::new(); + let mut initial_seigniorage_recipients = SeigniorageRecipientsSnapshotV2::new(); for era_id in initial_snapshot_range { initial_seigniorage_recipients.insert(era_id, seigniorage_recipients.clone()); } diff --git a/storage/src/system/protocol_upgrade.rs b/storage/src/system/protocol_upgrade.rs index 3cae245272..72442b44a6 100644 --- a/storage/src/system/protocol_upgrade.rs +++ b/storage/src/system/protocol_upgrade.rs @@ -13,7 +13,10 @@ use casper_types::{ bytesrepr::{self, ToBytes}, system::{ auction::{ - BidAddr, BidKind, ValidatorBid, AUCTION_DELAY_KEY, LOCKED_FUNDS_PERIOD_KEY, + BidAddr, BidKind, SeigniorageRecipientsSnapshotV1, SeigniorageRecipientsSnapshotV2, + SeigniorageRecipientsV2, ValidatorBid, AUCTION_DELAY_KEY, + DEFAULT_SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION, LOCKED_FUNDS_PERIOD_KEY, + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY, UNBONDING_DELAY_KEY, VALIDATOR_SLOTS_KEY, }, handle_payment::ACCUMULATION_PURSE_KEY, @@ -195,6 +198,7 @@ where self.config.maximum_delegation_amount(), )?; self.handle_era_info_migration()?; + self.handle_seignorage_snapshot_migration(system_entity_addresses.auction())?; Ok(self.tracking_copy) } @@ -1015,6 +1019,88 @@ where Ok(()) } + /// Handle seignorage snapshot migration to new version. + pub fn handle_seignorage_snapshot_migration( + &mut self, + auction: AddressableEntityHash, + ) -> Result<(), ProtocolUpgradeError> { + let auction_addr = EntityAddr::new_system(auction.value()); + let auction_named_keys = self.tracking_copy.get_named_keys(auction_addr)?; + let maybe_snapshot_version_key = + auction_named_keys.get(SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY); + let snapshot_key = auction_named_keys + .get(SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY) + .expect("snapshot key should already exist"); + + // if version flag does not exist yet, set it and migrate snapshot + if maybe_snapshot_version_key.is_none() { + // add new snapshot version named key + let snapshot_version_uref = self + .address_generator + .borrow_mut() + .new_uref(AccessRights::READ_ADD_WRITE); + let snapshot_version_uref_key = Key::URef(snapshot_version_uref); + + self.tracking_copy.write( + snapshot_version_uref_key, + StoredValue::CLValue(CLValue::from_t( + DEFAULT_SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION, + )?), + ); + + let auction_addr = EntityAddr::new_system(auction.value()); + let snapshot_version_named_key_addr = NamedKeyAddr::new_from_string( + auction_addr, + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY.to_string(), + ) + .map_err(|err| ProtocolUpgradeError::Bytesrepr(err.to_string()))?; + + let named_key_value = NamedKeyValue::from_concrete_values( + snapshot_version_uref_key, + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY.to_string(), + ) + .map_err(|error| ProtocolUpgradeError::CLValue(error.to_string()))?; + + self.tracking_copy.write( + Key::NamedKey(snapshot_version_named_key_addr), + StoredValue::NamedKey(named_key_value), + ); + + // read legacy snapshot + if let Some(snapshot_stored_value) = self.tracking_copy.read(snapshot_key)? { + let snapshot_cl_value = match snapshot_stored_value.into_cl_value() { + Some(cl_value) => cl_value, + None => { + error!("seigniorage recipients snapshot is not a CLValue"); + return Err(ProtocolUpgradeError::CLValue( + "seigniorage recipients snapshot is not a CLValue".to_string(), + )); + } + }; + + let legacy_snapshot: SeigniorageRecipientsSnapshotV1 = + snapshot_cl_value.into_t()?; + + let mut new_snapshot = SeigniorageRecipientsSnapshotV2::default(); + for (era_id, recipients) in legacy_snapshot.into_iter() { + let mut new_recipients = SeigniorageRecipientsV2::default(); + for (pubkey, recipient) in recipients { + new_recipients.insert(pubkey, recipient.into()); + } + new_snapshot.insert(era_id, new_recipients); + } + + // store new snapshot + self.tracking_copy.write( + *snapshot_key, + StoredValue::CLValue(CLValue::from_t(new_snapshot)?), + ); + }; + } + + Ok(()) + } + /// Handle global state updates. pub fn handle_global_state_updates(&mut self) { debug!("handle global state updates"); diff --git a/types/src/auction_state.rs b/types/src/auction_state.rs index 8e63e17d19..f7973da24d 100644 --- a/types/src/auction_state.rs +++ b/types/src/auction_state.rs @@ -51,6 +51,7 @@ static AUCTION_INFO: Lazy = Lazy::new(|| { DelegationRate::zero(), 0, u64::MAX, + 0, ); bids.push(BidKind::Validator(Box::new(validator_bid))); @@ -139,6 +140,7 @@ impl AuctionState { *bid.delegation_rate(), 0, u64::MAX, + 0, ); staking.insert(public_key, (validator_bid, bid.delegators().clone())); } diff --git a/types/src/gens.rs b/types/src/gens.rs index d8f7b63897..61838195e6 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -726,6 +726,7 @@ pub(crate) fn validator_bid_arb() -> impl Strategy { 1u64, 0, u64::MAX, + 0, ) } else { ValidatorBid::unlocked( @@ -735,6 +736,7 @@ pub(crate) fn validator_bid_arb() -> impl Strategy { delegation_rate, 0, u64::MAX, + 0, ) }; BidKind::Validator(Box::new(validator_bid)) diff --git a/types/src/system/auction.rs b/types/src/system/auction.rs index fafd876ced..949e046d87 100644 --- a/types/src/system/auction.rs +++ b/types/src/system/auction.rs @@ -1,4 +1,4 @@ -//! Contains implementation of a Auction contract functionality. +//! Contains implementation of the Auction contract functionality. mod bid; mod bid_addr; mod bid_kind; @@ -32,7 +32,9 @@ pub use entry_points::auction_entry_points; pub use era_info::{EraInfo, SeigniorageAllocation}; pub use error::Error; pub use reservation::Reservation; -pub use seigniorage_recipient::SeigniorageRecipient; +pub use seigniorage_recipient::{ + SeigniorageRecipient, SeigniorageRecipientV1, SeigniorageRecipientV2, +}; pub use unbonding_purse::UnbondingPurse; pub use validator_bid::ValidatorBid; pub use validator_credit::ValidatorCredit; @@ -54,6 +56,9 @@ pub type ValidatorBids = BTreeMap>; /// Delegator bids mapped to their validator. pub type DelegatorBids = BTreeMap>>; +/// Reservations mapped to their validator. +pub type Reservations = BTreeMap>>; + /// Validators mapped to their credits by era. pub type ValidatorCredits = BTreeMap>>; @@ -63,11 +68,48 @@ pub type ValidatorWeights = BTreeMap; /// List of era validators pub type EraValidators = BTreeMap; +/// Collection of seigniorage recipients. Legacy version. +pub type SeigniorageRecipientsV1 = BTreeMap; /// Collection of seigniorage recipients. -pub type SeigniorageRecipients = BTreeMap; +pub type SeigniorageRecipientsV2 = BTreeMap; +/// Wrapper enum for all variants of `SeigniorageRecipients`. +#[allow(missing_docs)] +pub enum SeigniorageRecipients { + V1(SeigniorageRecipientsV1), + V2(SeigniorageRecipientsV2), +} +/// Snapshot of `SeigniorageRecipients` for a given era. Legacy version. +pub type SeigniorageRecipientsSnapshotV1 = BTreeMap; /// Snapshot of `SeigniorageRecipients` for a given era. -pub type SeigniorageRecipientsSnapshot = BTreeMap; +pub type SeigniorageRecipientsSnapshotV2 = BTreeMap; +/// Wrapper enum for all variants of `SeigniorageRecipientsSnapshot`. +#[derive(Debug)] +#[allow(missing_docs)] +pub enum SeigniorageRecipientsSnapshot { + V1(SeigniorageRecipientsSnapshotV1), + V2(SeigniorageRecipientsSnapshotV2), +} + +impl SeigniorageRecipientsSnapshot { + /// Returns rewards for given validator in a specified era + pub fn get_seignorage_recipient( + &self, + era_id: &EraId, + validator_public_key: &PublicKey, + ) -> Option { + match self { + Self::V1(snapshot) => snapshot.get(era_id).and_then(|era| { + era.get(validator_public_key) + .map(|recipient| SeigniorageRecipient::V1(recipient.clone())) + }), + Self::V2(snapshot) => snapshot.get(era_id).and_then(|era| { + era.get(validator_public_key) + .map(|recipient| SeigniorageRecipient::V2(recipient.clone())) + }), + } + } +} /// Validators and delegators mapped to their unbonding purses. pub type UnbondingPurses = BTreeMap>; @@ -111,6 +153,12 @@ pub trait BidsExt { delegator_public_key: &PublicKey, ) -> Option; + /// Returns Reservation entries matching validator public key, if present. + fn reservations_by_validator_public_key( + &self, + public_key: &PublicKey, + ) -> Option>; + /// Returns Reservation entry by public keys, if present. fn reservation_by_public_keys( &self, @@ -240,6 +288,27 @@ impl BidsExt for Vec { } } + fn reservations_by_validator_public_key( + &self, + validator_public_key: &PublicKey, + ) -> Option> { + let mut ret = vec![]; + for reservation in self + .iter() + .filter(|x| x.is_reservation() && &x.validator_public_key() == validator_public_key) + { + if let BidKind::Reservation(reservation) = reservation { + ret.push(*reservation.clone()); + } + } + + if ret.is_empty() { + None + } else { + Some(ret) + } + } + fn reservation_by_public_keys( &self, validator_public_key: &PublicKey, diff --git a/types/src/system/auction/bid_addr.rs b/types/src/system/auction/bid_addr.rs index 2f01e00322..d856c72637 100644 --- a/types/src/system/auction/bid_addr.rs +++ b/types/src/system/auction/bid_addr.rs @@ -137,8 +137,8 @@ impl BidAddr { BidAddr::Validator(AccountHash::new(validator)) } - /// Constructs a new [`BidAddr`] instance from the [`AccountHash`] pair of a validator - /// and a delegator. + /// Constructs a new [`BidAddr::Delegator`] instance from the [`AccountHash`] pair of a + /// validator and a delegator. pub const fn new_delegator_addr( pair: ([u8; ACCOUNT_HASH_LENGTH], [u8; ACCOUNT_HASH_LENGTH]), ) -> Self { @@ -148,6 +148,17 @@ impl BidAddr { } } + /// Constructs a new [`BidAddr::Reservation`] instance from the [`AccountHash`] pair of a + /// validator and a delegator. + pub const fn new_reservation_addr( + pair: ([u8; ACCOUNT_HASH_LENGTH], [u8; ACCOUNT_HASH_LENGTH]), + ) -> Self { + BidAddr::Reservation { + validator: AccountHash::new(pair.0), + delegator: AccountHash::new(pair.1), + } + } + #[allow(missing_docs)] pub const fn legacy(validator: [u8; ACCOUNT_HASH_LENGTH]) -> Self { BidAddr::Unified(AccountHash::new(validator)) @@ -194,6 +205,16 @@ impl BidAddr { Ok(ret) } + /// Returns the common prefix of all reservations to the cited validator. + pub fn reservation_prefix(&self) -> Result, Error> { + let validator = self.validator_account_hash(); + let mut ret = Vec::with_capacity(validator.serialized_length() + 2); + ret.push(KeyTag::BidAddr as u8); + ret.push(BidAddrTag::Reservation as u8); + validator.write_bytes(&mut ret)?; + Ok(ret) + } + /// Validator account hash. pub fn validator_account_hash(&self) -> AccountHash { match self { @@ -314,6 +335,17 @@ impl FromBytes for BidAddr { let (era_id, remainder) = EraId::from_bytes(remainder)?; Ok((BidAddr::Credit { validator, era_id }, remainder)) } + tag if tag == BidAddrTag::Reservation as u8 => { + let (validator, remainder) = AccountHash::from_bytes(remainder)?; + let (delegator, remainder) = AccountHash::from_bytes(remainder)?; + Ok(( + BidAddr::Reservation { + validator, + delegator, + }, + remainder, + )) + } _ => Err(bytesrepr::Error::Formatting), } } @@ -420,6 +452,8 @@ mod tests { EraId::new(0), ); bytesrepr::test_serialization_roundtrip(&bid_addr); + let bid_addr = BidAddr::new_reservation_addr(([1; 32], [2; 32])); + bytesrepr::test_serialization_roundtrip(&bid_addr); } } diff --git a/types/src/system/auction/constants.rs b/types/src/system/auction/constants.rs index fc99c639b8..86fb464e6f 100644 --- a/types/src/system/auction/constants.rs +++ b/types/src/system/auction/constants.rs @@ -104,6 +104,12 @@ pub const ERA_ID_KEY: &str = "era_id"; pub const ERA_END_TIMESTAMP_MILLIS_KEY: &str = "era_end_timestamp_millis"; /// Storage for `SeigniorageRecipientsSnapshot`. pub const SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY: &str = "seigniorage_recipients_snapshot"; +/// Storage for a flag determining current version of `SeigniorageRecipientsSnapshot`. +pub const SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY: &str = + "seigniorage_recipients_snapshot_version"; +/// Default value for the current version of `SeigniorageRecipientsSnapshot`. +pub const DEFAULT_SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION: u8 = 2; + /// Total validator slots allowed. pub const VALIDATOR_SLOTS_KEY: &str = "validator_slots"; /// Amount of auction delay. diff --git a/types/src/system/auction/entry_points.rs b/types/src/system/auction/entry_points.rs index bdb708981f..58997a0536 100644 --- a/types/src/system/auction/entry_points.rs +++ b/types/src/system/auction/entry_points.rs @@ -1,8 +1,8 @@ use crate::{ system::auction::{ DelegationRate, ValidatorWeights, ARG_AMOUNT, ARG_DELEGATION_RATE, ARG_DELEGATOR, - ARG_ERA_END_TIMESTAMP_MILLIS, ARG_NEW_VALIDATOR, ARG_PUBLIC_KEY, ARG_VALIDATOR, - METHOD_ACTIVATE_BID, METHOD_ADD_BID, METHOD_DELEGATE, METHOD_DISTRIBUTE, + ARG_DELEGATORS, ARG_ERA_END_TIMESTAMP_MILLIS, ARG_NEW_VALIDATOR, ARG_PUBLIC_KEY, + ARG_VALIDATOR, METHOD_ACTIVATE_BID, METHOD_ADD_BID, METHOD_DELEGATE, METHOD_DISTRIBUTE, METHOD_GET_ERA_VALIDATORS, METHOD_READ_ERA_ID, METHOD_REDELEGATE, METHOD_RUN_AUCTION, METHOD_SLASH, METHOD_UNDELEGATE, METHOD_WITHDRAW_BID, }, @@ -12,9 +12,9 @@ use crate::{ use alloc::boxed::Box; use super::{ - Reservation, ARG_DELEGATORS, ARG_MAXIMUM_DELEGATION_AMOUNT, ARG_MINIMUM_DELEGATION_AMOUNT, - ARG_NEW_PUBLIC_KEY, ARG_RESERVATIONS, ARG_REWARDS_MAP, METHOD_ADD_RESERVATIONS, - METHOD_CANCEL_RESERVATIONS, METHOD_CHANGE_BID_PUBLIC_KEY, + Reservation, ARG_MAXIMUM_DELEGATION_AMOUNT, ARG_MINIMUM_DELEGATION_AMOUNT, ARG_NEW_PUBLIC_KEY, + ARG_RESERVATIONS, ARG_REWARDS_MAP, METHOD_ADD_RESERVATIONS, METHOD_CANCEL_RESERVATIONS, + METHOD_CHANGE_BID_PUBLIC_KEY, }; /// Creates auction contract entry points. diff --git a/types/src/system/auction/error.rs b/types/src/system/auction/error.rs index a126e085b0..0df857de91 100644 --- a/types/src/system/auction/error.rs +++ b/types/src/system/auction/error.rs @@ -365,6 +365,30 @@ pub enum Error { /// assert_eq!(55, Error::DelegationAmountTooLarge as u8); /// ``` DelegationAmountTooLarge = 55, + /// Reservation was not found in the map. + /// ``` + /// # use casper_types::system::auction::Error; + /// assert_eq!(56, Error::ReservationNotFound as u8); + /// ``` + ReservationNotFound = 56, + /// Validator exceeded allowed number of reserved delegator slots. + /// ``` + /// # use casper_types::system::auction::Error; + /// assert_eq!(57, Error::ExceededReservationSlotsLimit as u8); + /// ``` + ExceededReservationSlotsLimit = 57, + /// All reserved slots for validator are already occupied. + /// ``` + /// # use casper_types::system::auction::Error; + /// assert_eq!(58, Error::ExceededReservationsLimit as u8); + /// ``` + ExceededReservationsLimit = 58, + /// Reserved slots count is less than number of existing reservations. + /// ``` + /// # use casper_types::system::auction::Error; + /// assert_eq!(59, Error::ReservationSlotsCountTooSmall as u8); + /// ``` + ReservationSlotsCountTooSmall = 59, } impl Display for Error { @@ -426,6 +450,10 @@ impl Display for Error { Error::BridgeRecordChainTooLong => formatter.write_str("Bridge record chain is too long to find current validator bid"), Error::UnexpectedBidVariant => formatter.write_str("Unexpected bid variant"), Error::DelegationAmountTooLarge => formatter.write_str("The delegated amount is above the maximum allowed"), + Error::ReservationNotFound => formatter.write_str("Reservation not found"), + Error::ExceededReservationSlotsLimit => formatter.write_str("Validator exceeded allowed number of reserved delegator slots"), + Error::ExceededReservationsLimit => formatter.write_str("All reserved slots for validator are already occupied"), + Error::ReservationSlotsCountTooSmall => formatter.write_str("Reserved slots count is less than number of existing reservations") } } } @@ -515,6 +543,16 @@ impl TryFrom for Error { d if d == Error::BridgeRecordChainTooLong as u8 => Ok(Error::BridgeRecordChainTooLong), d if d == Error::UnexpectedBidVariant as u8 => Ok(Error::UnexpectedBidVariant), d if d == Error::DelegationAmountTooLarge as u8 => Ok(Error::DelegationAmountTooLarge), + d if d == Error::ReservationNotFound as u8 => Ok(Error::ReservationNotFound), + d if d == Error::ExceededReservationSlotsLimit as u8 => { + Ok(Error::ExceededReservationSlotsLimit) + } + d if d == Error::ExceededReservationsLimit as u8 => { + Ok(Error::ExceededReservationsLimit) + } + d if d == Error::ReservationSlotsCountTooSmall as u8 => { + Ok(Error::ReservationSlotsCountTooSmall) + } _ => Err(TryFromU8ForError(())), } } @@ -537,7 +575,7 @@ impl FromBytes for Error { let error: Error = value .try_into() // In case an Error variant is unable to be determined it would return an - // Error::Formatting as if its unable to be correctly deserialized. + // Error::Formatting as if it's unable to be correctly deserialized. .map_err(|_| bytesrepr::Error::Formatting)?; Ok((error, rem)) } diff --git a/types/src/system/auction/seigniorage_recipient.rs b/types/src/system/auction/seigniorage_recipient.rs index a82450f630..761aeb837f 100644 --- a/types/src/system/auction/seigniorage_recipient.rs +++ b/types/src/system/auction/seigniorage_recipient.rs @@ -7,8 +7,9 @@ use crate::{ }; /// The seigniorage recipient details. +/// Legacy version required to deserialize old records. #[derive(Default, PartialEq, Eq, Clone, Debug)] -pub struct SeigniorageRecipient { +pub struct SeigniorageRecipientV1 { /// Validator stake (not including delegators) stake: U512, /// Delegation rate of a seigniorage recipient. @@ -17,7 +18,7 @@ pub struct SeigniorageRecipient { delegator_stake: BTreeMap, } -impl SeigniorageRecipient { +impl SeigniorageRecipientV1 { /// Creates a new SeigniorageRecipient pub fn new( stake: U512, @@ -61,13 +62,13 @@ impl SeigniorageRecipient { } } -impl CLTyped for SeigniorageRecipient { +impl CLTyped for SeigniorageRecipientV1 { fn cl_type() -> CLType { CLType::Any } } -impl ToBytes for SeigniorageRecipient { +impl ToBytes for SeigniorageRecipientV1 { fn to_bytes(&self) -> Result, bytesrepr::Error> { let mut result = bytesrepr::allocate_buffer(self)?; result.extend(self.stake.to_bytes()?); @@ -83,13 +84,13 @@ impl ToBytes for SeigniorageRecipient { } } -impl FromBytes for SeigniorageRecipient { +impl FromBytes for SeigniorageRecipientV1 { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { let (stake, bytes) = FromBytes::from_bytes(bytes)?; let (delegation_rate, bytes) = FromBytes::from_bytes(bytes)?; let (delegator_stake, bytes) = FromBytes::from_bytes(bytes)?; Ok(( - SeigniorageRecipient { + Self { stake, delegation_rate, delegator_stake, @@ -99,7 +100,7 @@ impl FromBytes for SeigniorageRecipient { } } -impl From<&Bid> for SeigniorageRecipient { +impl From<&Bid> for SeigniorageRecipientV1 { fn from(bid: &Bid) -> Self { let delegator_stake = bid .delegators() @@ -114,14 +115,205 @@ impl From<&Bid> for SeigniorageRecipient { } } +/// The seigniorage recipient details with delegation rates for reservations. +#[derive(Default, PartialEq, Eq, Clone, Debug)] +pub struct SeigniorageRecipientV2 { + /// Validator stake (not including delegators) + stake: U512, + /// Delegation rate of a seigniorage recipient. + delegation_rate: DelegationRate, + /// Delegators and their bids. + delegator_stake: BTreeMap, + /// Delegation rates for reserved slots + reservation_delegation_rates: BTreeMap, +} + +impl SeigniorageRecipientV2 { + /// Creates a new SeigniorageRecipient + pub fn new( + stake: U512, + delegation_rate: DelegationRate, + delegator_stake: BTreeMap, + reservation_delegation_rates: BTreeMap, + ) -> Self { + Self { + stake, + delegation_rate, + delegator_stake, + reservation_delegation_rates, + } + } + + /// Returns stake of the provided recipient + pub fn stake(&self) -> &U512 { + &self.stake + } + + /// Returns delegation rate of the provided recipient + pub fn delegation_rate(&self) -> &DelegationRate { + &self.delegation_rate + } + + /// Returns delegators of the provided recipient and their stake + pub fn delegator_stake(&self) -> &BTreeMap { + &self.delegator_stake + } + + /// Calculates total stake, including delegators' total stake + pub fn total_stake(&self) -> Option { + self.delegator_total_stake()?.checked_add(self.stake) + } + + /// Calculates total stake for all delegators + pub fn delegator_total_stake(&self) -> Option { + let mut total_stake: U512 = U512::zero(); + for stake in self.delegator_stake.values() { + total_stake = total_stake.checked_add(*stake)?; + } + Some(total_stake) + } + + /// Returns delegation rates for reservations of the provided recipient + pub fn reservation_delegation_rates(&self) -> &BTreeMap { + &self.reservation_delegation_rates + } +} + +impl CLTyped for SeigniorageRecipientV2 { + fn cl_type() -> CLType { + CLType::Any + } +} + +impl ToBytes for SeigniorageRecipientV2 { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut result = bytesrepr::allocate_buffer(self)?; + result.extend(self.stake.to_bytes()?); + result.extend(self.delegation_rate.to_bytes()?); + result.extend(self.delegator_stake.to_bytes()?); + result.extend(self.reservation_delegation_rates.to_bytes()?); + Ok(result) + } + + fn serialized_length(&self) -> usize { + self.stake.serialized_length() + + self.delegation_rate.serialized_length() + + self.delegator_stake.serialized_length() + + self.reservation_delegation_rates.serialized_length() + } +} + +impl FromBytes for SeigniorageRecipientV2 { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (stake, bytes) = FromBytes::from_bytes(bytes)?; + let (delegation_rate, bytes) = FromBytes::from_bytes(bytes)?; + let (delegator_stake, bytes) = FromBytes::from_bytes(bytes)?; + let (reservation_delegation_rates, bytes) = FromBytes::from_bytes(bytes)?; + Ok(( + Self { + stake, + delegation_rate, + delegator_stake, + reservation_delegation_rates, + }, + bytes, + )) + } +} + +impl From<&Bid> for SeigniorageRecipientV2 { + fn from(bid: &Bid) -> Self { + let delegator_stake = bid + .delegators() + .iter() + .map(|(public_key, delegator)| (public_key.clone(), delegator.staked_amount())) + .collect(); + Self { + stake: *bid.staked_amount(), + delegation_rate: *bid.delegation_rate(), + delegator_stake, + reservation_delegation_rates: BTreeMap::new(), + } + } +} + +impl From for SeigniorageRecipientV2 { + fn from(snapshot: SeigniorageRecipientV1) -> Self { + Self { + stake: snapshot.stake, + delegation_rate: snapshot.delegation_rate, + delegator_stake: snapshot.delegator_stake, + reservation_delegation_rates: Default::default(), + } + } +} + +/// Wrapper enum for all variants of `SeigniorageRecipient`. +#[allow(missing_docs)] +pub enum SeigniorageRecipient { + V1(SeigniorageRecipientV1), + V2(SeigniorageRecipientV2), +} + +impl SeigniorageRecipient { + /// Returns stake of the provided recipient + pub fn stake(&self) -> &U512 { + match self { + Self::V1(recipient) => &recipient.stake, + Self::V2(recipient) => &recipient.stake, + } + } + + /// Returns delegation rate of the provided recipient + pub fn delegation_rate(&self) -> &DelegationRate { + match self { + Self::V1(recipient) => &recipient.delegation_rate, + Self::V2(recipient) => &recipient.delegation_rate, + } + } + + /// Returns delegators of the provided recipient and their stake + pub fn delegator_stake(&self) -> &BTreeMap { + match self { + Self::V1(recipient) => &recipient.delegator_stake, + Self::V2(recipient) => &recipient.delegator_stake, + } + } + + /// Calculates total stake, including delegators' total stake + pub fn total_stake(&self) -> Option { + match self { + Self::V1(recipient) => recipient.total_stake(), + Self::V2(recipient) => recipient.total_stake(), + } + } + + /// Calculates total stake for all delegators + pub fn delegator_total_stake(&self) -> Option { + match self { + Self::V1(recipient) => recipient.delegator_total_stake(), + Self::V2(recipient) => recipient.delegator_total_stake(), + } + } + + /// Returns delegation rates for reservations of the provided recipient + pub fn reservation_delegation_rates(&self) -> Option<&BTreeMap> { + match self { + Self::V1(_recipient) => None, + Self::V2(recipient) => Some(&recipient.reservation_delegation_rates), + } + } +} + #[cfg(test)] mod tests { use alloc::collections::BTreeMap; use core::iter::FromIterator; + use super::SeigniorageRecipientV2; use crate::{ bytesrepr, - system::auction::{DelegationRate, SeigniorageRecipient}, + system::auction::{DelegationRate, SeigniorageRecipientV1}, PublicKey, SecretKey, U512, }; @@ -136,18 +328,46 @@ mod tests { let delegator_3_key = PublicKey::from( &SecretKey::ed25519_from_bytes([44; SecretKey::ED25519_LENGTH]).unwrap(), ); - let seigniorage_recipient = SeigniorageRecipient { + let seigniorage_recipient = SeigniorageRecipientV2 { stake: U512::max_value(), - delegation_rate: DelegationRate::max_value(), + delegation_rate: DelegationRate::MAX, delegator_stake: BTreeMap::from_iter(vec![ - (delegator_1_key, U512::max_value()), + (delegator_1_key.clone(), U512::max_value()), (delegator_2_key, U512::max_value()), (delegator_3_key, U512::zero()), ]), + reservation_delegation_rates: BTreeMap::from_iter(vec![( + delegator_1_key, + DelegationRate::MIN, + )]), }; bytesrepr::test_serialization_roundtrip(&seigniorage_recipient); } + #[test] + fn serialization_roundtrip_legacy_version() { + let delegator_1_key = PublicKey::from( + &SecretKey::ed25519_from_bytes([42; SecretKey::ED25519_LENGTH]).unwrap(), + ); + let delegator_2_key = PublicKey::from( + &SecretKey::ed25519_from_bytes([43; SecretKey::ED25519_LENGTH]).unwrap(), + ); + let delegator_3_key = PublicKey::from( + &SecretKey::ed25519_from_bytes([44; SecretKey::ED25519_LENGTH]).unwrap(), + ); + let legacy_seigniorage_recipient = SeigniorageRecipientV1 { + stake: U512::max_value(), + delegation_rate: DelegationRate::MAX, + delegator_stake: BTreeMap::from_iter(vec![ + (delegator_1_key.clone(), U512::max_value()), + (delegator_2_key.clone(), U512::max_value()), + (delegator_3_key.clone(), U512::zero()), + ]), + }; + + bytesrepr::test_serialization_roundtrip(&legacy_seigniorage_recipient); + } + #[test] fn test_overflow_in_delegation_rate() { let delegator_1_key = PublicKey::from( @@ -159,14 +379,18 @@ mod tests { let delegator_3_key = PublicKey::from( &SecretKey::ed25519_from_bytes([44; SecretKey::ED25519_LENGTH]).unwrap(), ); - let seigniorage_recipient = SeigniorageRecipient { + let seigniorage_recipient = SeigniorageRecipientV2 { stake: U512::max_value(), - delegation_rate: DelegationRate::max_value(), + delegation_rate: DelegationRate::MAX, delegator_stake: BTreeMap::from_iter(vec![ - (delegator_1_key, U512::max_value()), + (delegator_1_key.clone(), U512::max_value()), (delegator_2_key, U512::max_value()), (delegator_3_key, U512::zero()), ]), + reservation_delegation_rates: BTreeMap::from_iter(vec![( + delegator_1_key, + DelegationRate::MIN, + )]), }; assert_eq!(seigniorage_recipient.total_stake(), None) } @@ -182,7 +406,7 @@ mod tests { let delegator_3_key = PublicKey::from( &SecretKey::ed25519_from_bytes([44; SecretKey::ED25519_LENGTH]).unwrap(), ); - let seigniorage_recipient = SeigniorageRecipient { + let seigniorage_recipient = SeigniorageRecipientV2 { stake: U512::max_value(), delegation_rate: DelegationRate::max_value(), delegator_stake: BTreeMap::from_iter(vec![ @@ -190,6 +414,7 @@ mod tests { (delegator_2_key, U512::max_value()), (delegator_3_key, U512::max_value()), ]), + reservation_delegation_rates: BTreeMap::new(), }; assert_eq!(seigniorage_recipient.delegator_total_stake(), None) } diff --git a/types/src/system/auction/validator_bid.rs b/types/src/system/auction/validator_bid.rs index 3f3f92660b..bf8795e05f 100644 --- a/types/src/system/auction/validator_bid.rs +++ b/types/src/system/auction/validator_bid.rs @@ -58,6 +58,7 @@ impl ValidatorBid { impl ValidatorBid { /// Creates new instance of a bid with locked funds. + #[allow(clippy::too_many_arguments)] pub fn locked( validator_public_key: PublicKey, bonding_purse: URef, @@ -66,6 +67,7 @@ impl ValidatorBid { release_timestamp_millis: u64, minimum_delegation_amount: u64, maximum_delegation_amount: u64, + reserved_slots: u32, ) -> Self { let vesting_schedule = Some(VestingSchedule::new(release_timestamp_millis)); let inactive = false; @@ -78,7 +80,7 @@ impl ValidatorBid { inactive, minimum_delegation_amount, maximum_delegation_amount, - reserved_slots: 0, + reserved_slots, } } @@ -90,6 +92,7 @@ impl ValidatorBid { delegation_rate: DelegationRate, minimum_delegation_amount: u64, maximum_delegation_amount: u64, + reserved_slots: u32, ) -> Self { let vesting_schedule = None; let inactive = false; @@ -102,7 +105,7 @@ impl ValidatorBid { inactive, minimum_delegation_amount, maximum_delegation_amount, - reserved_slots: 0, + reserved_slots, } } @@ -187,6 +190,11 @@ impl ValidatorBid { self.vesting_schedule.as_mut() } + /// Gets the reserved slots of the provided bid + pub fn reserved_slots(&self) -> u32 { + self.reserved_slots + } + /// Returns `true` if validator is inactive pub fn inactive(&self) -> bool { self.inactive @@ -245,6 +253,12 @@ impl ValidatorBid { self } + /// Updates the reserved slots of the provided bid + pub fn with_reserved_slots(&mut self, reserved_slots: u32) -> &mut Self { + self.reserved_slots = reserved_slots; + self + } + /// Sets given bid's `inactive` field to `false` pub fn activate(&mut self) -> bool { self.inactive = false; @@ -439,6 +453,7 @@ mod tests { validator_release_timestamp, 0, u64::MAX, + 0, ); assert!(!bid.is_locked_with_vesting_schedule( diff --git a/types/src/transaction/transaction_v1/transaction_v1_body.rs b/types/src/transaction/transaction_v1/transaction_v1_body.rs index fec2259311..e1a00ae0cd 100644 --- a/types/src/transaction/transaction_v1/transaction_v1_body.rs +++ b/types/src/transaction/transaction_v1/transaction_v1_body.rs @@ -209,10 +209,10 @@ impl TransactionV1Body { arg_handling::has_valid_change_bid_public_key_args(&self.args) } TransactionEntryPoint::AddReservations => { - todo!() + arg_handling::has_valid_add_reservations_args(&self.args) } TransactionEntryPoint::CancelReservations => { - todo!() + arg_handling::has_valid_cancel_reservations_args(&self.args) } }, TransactionTarget::Stored { .. } => match &self.entry_point { diff --git a/types/src/transaction/transaction_v1/transaction_v1_body/arg_handling.rs b/types/src/transaction/transaction_v1/transaction_v1_body/arg_handling.rs index a136ce3cae..993d7c267b 100644 --- a/types/src/transaction/transaction_v1/transaction_v1_body/arg_handling.rs +++ b/types/src/transaction/transaction_v1/transaction_v1_body/arg_handling.rs @@ -6,6 +6,7 @@ use tracing::debug; use crate::{account::AccountHash, system::auction::ARG_VALIDATOR, CLType}; use crate::{ bytesrepr::{FromBytes, ToBytes}, + system::auction::Reservation, CLTyped, CLValue, CLValueError, InvalidTransactionV1, PublicKey, RuntimeArgs, TransferTarget, URef, U512, }; @@ -46,6 +47,13 @@ const CHANGE_BID_PUBLIC_KEY_ARG_PUBLIC_KEY: RequiredArg = RequiredArg const CHANGE_BID_PUBLIC_KEY_ARG_NEW_PUBLIC_KEY: RequiredArg = RequiredArg::new("new_public_key"); +const ADD_RESERVATIONS_ARG_RESERVATIONS: RequiredArg> = + RequiredArg::new("reservations"); + +const CANCEL_RESERVATIONS_ARG_VALIDATOR: RequiredArg = RequiredArg::new("validator"); +const CANCEL_RESERVATIONS_ARG_DELEGATORS: RequiredArg> = + RequiredArg::new("delegators"); + struct RequiredArg { name: &'static str, _phantom: PhantomData, @@ -354,7 +362,6 @@ pub(in crate::transaction::transaction_v1) fn has_valid_activate_bid_args( } /// Checks the given `RuntimeArgs` are suitable for use in a change bid public key transaction. -#[allow(dead_code)] pub(super) fn has_valid_change_bid_public_key_args( args: &RuntimeArgs, ) -> Result<(), InvalidTransactionV1> { @@ -363,6 +370,23 @@ pub(super) fn has_valid_change_bid_public_key_args( Ok(()) } +/// Checks the given `RuntimeArgs` are suitable for use in a add reservations transaction. +pub(super) fn has_valid_add_reservations_args( + args: &RuntimeArgs, +) -> Result<(), InvalidTransactionV1> { + let _reservations = ADD_RESERVATIONS_ARG_RESERVATIONS.get(args)?; + Ok(()) +} + +/// Checks the given `RuntimeArgs` are suitable for use in a add reservations transaction. +pub(super) fn has_valid_cancel_reservations_args( + args: &RuntimeArgs, +) -> Result<(), InvalidTransactionV1> { + let _validator = CANCEL_RESERVATIONS_ARG_VALIDATOR.get(args)?; + let _delegators = CANCEL_RESERVATIONS_ARG_DELEGATORS.get(args)?; + Ok(()) +} + #[cfg(test)] mod tests { use rand::Rng; diff --git a/utils/global-state-update-gen/src/decode.rs b/utils/global-state-update-gen/src/decode.rs index d841f280e3..49433465c7 100644 --- a/utils/global-state-update-gen/src/decode.rs +++ b/utils/global-state-update-gen/src/decode.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, fmt, fs::File, io::Read}; use clap::ArgMatches; use casper_types::{ - bytesrepr::FromBytes, system::auction::SeigniorageRecipientsSnapshot, CLType, + bytesrepr::FromBytes, system::auction::SeigniorageRecipientsSnapshotV2, CLType, GlobalStateUpdate, GlobalStateUpdateConfig, Key, StoredValue, }; @@ -17,7 +17,8 @@ impl fmt::Debug for Entries { StoredValue::CLValue(clv) => match clv.cl_type() { CLType::Map { key, value: _ } if **key == CLType::U64 => { // this should be the seigniorage recipient snapshot - let snapshot: SeigniorageRecipientsSnapshot = clv.clone().into_t().unwrap(); + let snapshot: SeigniorageRecipientsSnapshotV2 = + clv.clone().into_t().unwrap(); Box::new(snapshot) } _ => Box::new(clv), diff --git a/utils/global-state-update-gen/src/generic.rs b/utils/global-state-update-gen/src/generic.rs index dd2fa76056..e02b387310 100644 --- a/utils/global-state-update-gen/src/generic.rs +++ b/utils/global-state-update-gen/src/generic.rs @@ -17,8 +17,8 @@ use casper_engine_test_support::LmdbWasmTestBuilder; use casper_execution_engine::engine_state::engine_config::DEFAULT_PROTOCOL_VERSION; use casper_types::{ system::auction::{ - Bid, BidKind, BidsExt, Delegator, Reservation, SeigniorageRecipient, - SeigniorageRecipientsSnapshot, ValidatorBid, ValidatorCredit, + Bid, BidKind, BidsExt, Delegator, Reservation, SeigniorageRecipientV2, + SeigniorageRecipientsSnapshotV2, ValidatorBid, ValidatorCredit, }, CLValue, EraId, PublicKey, StoredValue, U512, }; @@ -173,7 +173,7 @@ fn update_auction_state( ) } -/// Generates a new `SeigniorageRecipientsSnapshot` based on: +/// Generates a new `SeigniorageRecipientsSnapshotV2` based on: /// - The starting era ID (the era ID at which the snapshot should start). /// - Count - the number of eras to be included in the snapshot. /// - The list of configured accounts. @@ -181,7 +181,7 @@ fn gen_snapshot_only_listed( starting_era_id: EraId, count: u64, accounts: &[AccountConfig], -) -> SeigniorageRecipientsSnapshot { +) -> SeigniorageRecipientsSnapshotV2 { let mut new_snapshot = BTreeMap::new(); let mut era_validators = BTreeMap::new(); for account in accounts { @@ -190,10 +190,11 @@ fn gen_snapshot_only_listed( Some(validator) if validator.bonded_amount != U512::zero() => validator, _ => continue, }; - let seigniorage_recipient = SeigniorageRecipient::new( + let seigniorage_recipient = SeigniorageRecipientV2::new( validator_cfg.bonded_amount, validator_cfg.delegation_rate.unwrap_or_default(), validator_cfg.delegators_map().unwrap_or_default(), + validator_cfg.reservations_map().unwrap_or_default(), ); let _ = era_validators.insert(account.public_key.clone(), seigniorage_recipient); } @@ -203,12 +204,12 @@ fn gen_snapshot_only_listed( new_snapshot } -/// Generates a new `SeigniorageRecipientsSnapshot` by modifying the stakes listed in the old +/// Generates a new `SeigniorageRecipientsSnapshotV2` by modifying the stakes listed in the old /// snapshot according to the supplied list of configured accounts. fn gen_snapshot_from_old( - mut snapshot: SeigniorageRecipientsSnapshot, + mut snapshot: SeigniorageRecipientsSnapshotV2, accounts: &[AccountConfig], -) -> SeigniorageRecipientsSnapshot { +) -> SeigniorageRecipientsSnapshotV2 { // Read the modifications to be applied to the validators set from the config. let validators_map: BTreeMap<_, _> = accounts .iter() @@ -230,7 +231,7 @@ fn gen_snapshot_from_old( Some(validator) if validator.bonded_amount.is_zero() => false, // Otherwise, we keep them, but modify the properties. Some(validator) => { - *recipient = SeigniorageRecipient::new( + *recipient = SeigniorageRecipientV2::new( validator.bonded_amount, validator .delegation_rate @@ -242,6 +243,11 @@ fn gen_snapshot_from_old( // If the delegators weren't specified in the config, keep the ones // from the old snapshot. .unwrap_or_else(|| recipient.delegator_stake().clone()), + validator + .reservations_map() + // If the delegators weren't specified in the config, keep the ones + // from the old snapshot. + .unwrap_or_else(|| recipient.reservation_delegation_rates().clone()), ); true } @@ -259,12 +265,15 @@ fn gen_snapshot_from_old( if validator.bonded_amount != U512::zero() { recipients.insert( public_key.clone(), - SeigniorageRecipient::new( + SeigniorageRecipientV2::new( validator.bonded_amount, // Unspecified delegation rate will be treated as 0. validator.delegation_rate.unwrap_or_default(), // Unspecified delegators will be treated as an empty list. validator.delegators_map().unwrap_or_default(), + // Unspecified reservation delegation rates will be treated as an empty + // list. + validator.reservations_map().unwrap_or_default(), ), ); } @@ -284,7 +293,7 @@ fn gen_snapshot_from_old( pub fn add_and_remove_bids( state: &mut StateTracker, validators_diff: &ValidatorsDiff, - new_snapshot: &SeigniorageRecipientsSnapshot, + new_snapshot: &SeigniorageRecipientsSnapshotV2, only_listed_validators: bool, slash_instead_of_unbonding: bool, ) { @@ -360,7 +369,7 @@ pub fn add_and_remove_bids( /// validators. fn find_large_bids( state: &mut StateTracker, - snapshot: &SeigniorageRecipientsSnapshot, + snapshot: &SeigniorageRecipientsSnapshotV2, ) -> BTreeSet { let seigniorage_recipients = snapshot.values().next().unwrap(); let min_bid = seigniorage_recipients @@ -425,7 +434,7 @@ fn find_large_bids( fn create_or_update_bid( state: &mut StateTracker, validator_public_key: &PublicKey, - updated_recipient: &SeigniorageRecipient, + updated_recipient: &SeigniorageRecipientV2, slash_instead_of_unbonding: bool, ) { let existing_bids = state.get_bids(); @@ -436,27 +445,46 @@ fn create_or_update_bid( (x.is_unified() || x.is_validator()) && &x.validator_public_key() == validator_public_key }) - .map(|existing_bid| match existing_bid { - BidKind::Unified(bid) => { - let delegator_stake = bid - .delegators() - .iter() - .map(|(k, d)| (k.clone(), d.staked_amount())) - .collect(); - ( - bid.bonding_purse(), - SeigniorageRecipient::new( - *bid.staked_amount(), - *bid.delegation_rate(), - delegator_stake, - ), - 0, - u64::MAX, - ) - } - BidKind::Validator(validator_bid) => { - let delegator_stake = - match existing_bids.delegators_by_validator_public_key(validator_public_key) { + .map(|existing_bid| { + let reservation_delegation_rates = + match existing_bids.reservations_by_validator_public_key(validator_public_key) { + None => BTreeMap::new(), + Some(reservations) => reservations + .iter() + .map(|reservation| { + ( + reservation.delegator_public_key().clone(), + *reservation.delegation_rate(), + ) + }) + .collect(), + }; + + match existing_bid { + BidKind::Unified(bid) => { + let delegator_stake = bid + .delegators() + .iter() + .map(|(k, d)| (k.clone(), d.staked_amount())) + .collect(); + + ( + bid.bonding_purse(), + SeigniorageRecipientV2::new( + *bid.staked_amount(), + *bid.delegation_rate(), + delegator_stake, + reservation_delegation_rates, + ), + 0, + u64::MAX, + 0, + ) + } + BidKind::Validator(validator_bid) => { + let delegator_stake = match existing_bids + .delegators_by_validator_public_key(validator_public_key) + { None => BTreeMap::new(), Some(delegators) => delegators .iter() @@ -464,23 +492,31 @@ fn create_or_update_bid( .collect(), }; - ( - validator_bid.bonding_purse(), - SeigniorageRecipient::new( - validator_bid.staked_amount(), - *validator_bid.delegation_rate(), - delegator_stake, - ), - validator_bid.minimum_delegation_amount(), - validator_bid.maximum_delegation_amount(), - ) + ( + validator_bid.bonding_purse(), + SeigniorageRecipientV2::new( + validator_bid.staked_amount(), + *validator_bid.delegation_rate(), + delegator_stake, + reservation_delegation_rates, + ), + validator_bid.minimum_delegation_amount(), + validator_bid.maximum_delegation_amount(), + validator_bid.reserved_slots(), + ) + } + _ => unreachable!(), } - _ => unreachable!(), }); // existing bid - if let Some((bonding_purse, existing_recipient, min_delegation_amount, max_delegation_amount)) = - maybe_existing_recipient + if let Some(( + bonding_purse, + existing_recipient, + min_delegation_amount, + max_delegation_amount, + reserved_slots, + )) = maybe_existing_recipient { if existing_recipient == *updated_recipient { return; // noop @@ -557,6 +593,7 @@ fn create_or_update_bid( *updated_recipient.delegation_rate(), min_delegation_amount, max_delegation_amount, + reserved_slots, ); state.set_bid( @@ -595,6 +632,7 @@ fn create_or_update_bid( *updated_recipient.delegation_rate(), 0, u64::MAX, + 0, ); state.set_bid( BidKind::Validator(Box::new(validator_bid)), diff --git a/utils/global-state-update-gen/src/generic/config.rs b/utils/global-state-update-gen/src/generic/config.rs index bbc5f3064c..873718df82 100644 --- a/utils/global-state-update-gen/src/generic/config.rs +++ b/utils/global-state-update-gen/src/generic/config.rs @@ -2,7 +2,9 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; -use casper_types::{account::AccountHash, ProtocolVersion, PublicKey, U512}; +use casper_types::{ + account::AccountHash, system::auction::DelegationRate, ProtocolVersion, PublicKey, U512, +}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Config { @@ -37,6 +39,7 @@ pub struct ValidatorConfig { pub bonded_amount: U512, pub delegation_rate: Option, pub delegators: Option>, + pub reservations: Option>, } impl ValidatorConfig { @@ -48,6 +51,15 @@ impl ValidatorConfig { .collect() }) } + + pub fn reservations_map(&self) -> Option> { + self.reservations.as_ref().map(|reservations| { + reservations + .iter() + .map(|reservation| (reservation.public_key.clone(), reservation.delegation_rate)) + .collect() + }) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -55,3 +67,9 @@ pub struct DelegatorConfig { pub public_key: PublicKey, pub delegated_amount: U512, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReservationConfig { + pub public_key: PublicKey, + pub delegation_rate: DelegationRate, +} diff --git a/utils/global-state-update-gen/src/generic/state_tracker.rs b/utils/global-state-update-gen/src/generic/state_tracker.rs index c2de88a199..b2b4f78832 100644 --- a/utils/global-state-update-gen/src/generic/state_tracker.rs +++ b/utils/global-state-update-gen/src/generic/state_tracker.rs @@ -10,8 +10,8 @@ use casper_types::{ account::AccountHash, addressable_entity::{ActionThresholds, AssociatedKeys, MessageTopics, Weight}, system::auction::{ - BidAddr, BidKind, BidsExt, SeigniorageRecipientsSnapshot, UnbondingPurse, UnbondingPurses, - WithdrawPurse, WithdrawPurses, + BidAddr, BidKind, BidsExt, SeigniorageRecipientsSnapshotV2, UnbondingPurse, + UnbondingPurses, WithdrawPurse, WithdrawPurses, }, AccessRights, AddressableEntity, AddressableEntityHash, ByteCodeHash, CLValue, EntityKind, EntityVersions, Groups, Key, Package, PackageHash, PackageStatus, ProtocolVersion, PublicKey, @@ -31,7 +31,7 @@ pub struct StateTracker { unbonds_cache: BTreeMap>, purses_cache: BTreeMap, staking: Option>, - seigniorage_recipients: Option<(Key, SeigniorageRecipientsSnapshot)>, + seigniorage_recipients: Option<(Key, SeigniorageRecipientsSnapshotV2)>, protocol_version: ProtocolVersion, } @@ -265,7 +265,7 @@ impl StateTracker { } /// Reads the `SeigniorageRecipientsSnapshot` stored in the global state. - pub fn read_snapshot(&mut self) -> (Key, SeigniorageRecipientsSnapshot) { + pub fn read_snapshot(&mut self) -> (Key, SeigniorageRecipientsSnapshotV2) { if let Some(key_and_snapshot) = &self.seigniorage_recipients { return key_and_snapshot.clone(); } @@ -275,7 +275,7 @@ impl StateTracker { // Decode the old snapshot. let stored_value = self.reader.query(validators_key).expect("should query"); let cl_value = stored_value.into_cl_value().expect("should be cl value"); - let snapshot: SeigniorageRecipientsSnapshot = cl_value.into_t().expect("should convert"); + let snapshot: SeigniorageRecipientsSnapshotV2 = cl_value.into_t().expect("should convert"); self.seigniorage_recipients = Some((validators_key, snapshot.clone())); (validators_key, snapshot) } diff --git a/utils/global-state-update-gen/src/generic/testing.rs b/utils/global-state-update-gen/src/generic/testing.rs index cd9a1de5a1..46fbaa2150 100644 --- a/utils/global-state-update-gen/src/generic/testing.rs +++ b/utils/global-state-update-gen/src/generic/testing.rs @@ -7,9 +7,9 @@ use casper_types::{ account::AccountHash, addressable_entity::{ActionThresholds, AssociatedKeys, MessageTopics, Weight}, system::auction::{ - BidKind, BidsExt, Delegator, SeigniorageRecipient, SeigniorageRecipients, - SeigniorageRecipientsSnapshot, UnbondingPurse, UnbondingPurses, ValidatorBid, - WithdrawPurse, WithdrawPurses, + BidKind, BidsExt, Delegator, SeigniorageRecipientV2, SeigniorageRecipientsSnapshotV2, + SeigniorageRecipientsV2, UnbondingPurse, UnbondingPurses, ValidatorBid, WithdrawPurse, + WithdrawPurses, }, testing::TestRng, AccessRights, AddressableEntity, ByteCodeHash, CLValue, EntityKind, EraId, Key, PackageHash, @@ -32,7 +32,7 @@ struct MockStateReader { accounts: BTreeMap, purses: BTreeMap, total_supply: U512, - seigniorage_recipients: SeigniorageRecipientsSnapshot, + seigniorage_recipients: SeigniorageRecipientsSnapshotV2, bids: Vec, withdraws: WithdrawPurses, unbonds: UnbondingPurses, @@ -45,7 +45,7 @@ impl MockStateReader { accounts: BTreeMap::new(), purses: BTreeMap::new(), total_supply: U512::zero(), - seigniorage_recipients: SeigniorageRecipientsSnapshot::new(), + seigniorage_recipients: SeigniorageRecipientsSnapshotV2::new(), bids: vec![], withdraws: WithdrawPurses::new(), unbonds: UnbondingPurses::new(), @@ -84,13 +84,19 @@ impl MockStateReader { validators: Vec<(PublicKey, U512, ValidatorConfig)>, rng: &mut R, ) -> Self { - let mut recipients = SeigniorageRecipients::new(); + let mut recipients = SeigniorageRecipientsV2::new(); for (public_key, balance, validator_cfg) in validators { let stake = validator_cfg.bonded_amount; let delegation_rate = validator_cfg.delegation_rate.unwrap_or_default(); let delegators = validator_cfg.delegators_map().unwrap_or_default(); + let reservation_delegation_rates = validator_cfg.reservations_map().unwrap_or_default(); // add an entry to the recipients snapshot - let recipient = SeigniorageRecipient::new(stake, delegation_rate, delegators.clone()); + let recipient = SeigniorageRecipientV2::new( + stake, + delegation_rate, + delegators.clone(), + reservation_delegation_rates, + ); recipients.insert(public_key.clone(), recipient); // create the account if it doesn't exist @@ -133,6 +139,7 @@ impl MockStateReader { delegation_rate, 0, u64::MAX, + 0, ); self.bids.push(BidKind::Validator(Box::new(validator_bid))); @@ -506,6 +513,7 @@ fn should_change_one_validator() { bonded_amount: validator3_new_staked, delegation_rate: None, delegators: None, + reservations: None, }), }], ..Default::default() @@ -543,6 +551,7 @@ fn should_change_one_validator() { Default::default(), 0, u64::MAX, + 0, ); update.assert_written_bid(account3_hash, BidKind::Validator(Box::new(expected_bid))); @@ -602,6 +611,7 @@ fn should_change_only_stake_of_one_validator() { bonded_amount: U512::from(104), delegation_rate: None, delegators: None, + reservations: None, }), }], ..Default::default() @@ -637,6 +647,7 @@ fn should_change_only_stake_of_one_validator() { Default::default(), 0, u64::MAX, + 0, ); update.assert_written_bid(account3_hash, BidKind::Validator(Box::new(expected_bid))); @@ -745,6 +756,7 @@ fn should_replace_one_validator() { bonded_amount: U512::from(102), delegation_rate: None, delegators: None, + reservations: None, }), }], only_listed_validators: true, @@ -778,6 +790,7 @@ fn should_replace_one_validator() { Default::default(), 0, u64::MAX, + 0, ); expected_bid_1.deactivate(); update.assert_written_bid(account1_hash, BidKind::Validator(Box::new(expected_bid_1))); @@ -884,6 +897,7 @@ fn should_replace_one_validator_with_unbonding() { Default::default(), 0, u64::MAX, + 0, ); expected_bid_1.deactivate(); update.assert_written_bid(account1_hash, BidKind::Validator(Box::new(expected_bid_1))); @@ -975,6 +989,7 @@ fn should_add_one_validator() { bonded_amount: v4_stake, delegation_rate: None, delegators: None, + reservations: None, }), }], only_listed_validators: false, @@ -1065,6 +1080,7 @@ fn should_add_one_validator_with_delegators() { public_key: delegator1.clone(), delegated_amount: U512::from(13), }]), + reservations: None, }), }], only_listed_validators: false, @@ -1157,6 +1173,7 @@ fn should_replace_a_delegator() { public_key: delegator1.clone(), delegated_amount: U512::from(d1_stake), }]), + reservations: None, }, )], &mut rng, @@ -1174,6 +1191,7 @@ fn should_replace_a_delegator() { public_key: delegator2.clone(), delegated_amount: U512::from(d2_stake), }]), + reservations: None, }), }], only_listed_validators: false, @@ -1259,6 +1277,7 @@ fn should_replace_a_delegator_with_unbonding() { public_key: delegator1.clone(), delegated_amount: U512::from(d1_stake), }]), + reservations: None, }, )], &mut rng, @@ -1276,6 +1295,7 @@ fn should_replace_a_delegator_with_unbonding() { public_key: delegator2.clone(), delegated_amount: U512::from(d2_stake), }]), + reservations: None, }), }], only_listed_validators: false, @@ -1364,6 +1384,7 @@ fn should_not_change_the_delegator() { public_key: delegator1, delegated_amount: U512::from(d1_stake), }]), + reservations: None, }, )], &mut rng, @@ -1378,6 +1399,7 @@ fn should_not_change_the_delegator() { bonded_amount: U512::from(v1_updated_stake), delegation_rate: None, delegators: None, + reservations: None, }), }], only_listed_validators: false, @@ -1444,6 +1466,7 @@ fn should_remove_the_delegator() { public_key: delegator1.clone(), delegated_amount: d_stake, }]), + reservations: None, }, )], &mut rng, @@ -1493,6 +1516,7 @@ fn should_remove_the_delegator() { bonded_amount: v_updated_stake, delegation_rate: None, delegators: Some(vec![]), + reservations: None, }), }], only_listed_validators: false, @@ -1557,6 +1581,7 @@ fn should_remove_the_delegator_with_unbonding() { public_key: delegator1.clone(), delegated_amount: U512::from(13), }]), + reservations: None, }, )], &mut rng, @@ -1571,6 +1596,7 @@ fn should_remove_the_delegator_with_unbonding() { bonded_amount: U512::from(111), delegation_rate: None, delegators: Some(vec![]), + reservations: None, }), }], only_listed_validators: false, @@ -1651,6 +1677,7 @@ fn should_slash_a_validator_and_delegator_with_enqueued_withdraws() { public_key: delegator1.clone(), delegated_amount: amount, }]), + reservations: None, }; let mut reader = MockStateReader::new() @@ -1667,6 +1694,7 @@ fn should_slash_a_validator_and_delegator_with_enqueued_withdraws() { public_key: delegator2.clone(), delegated_amount: amount, }]), + reservations: None, }, ), ], @@ -1797,6 +1825,7 @@ fn should_slash_a_validator_and_delegator_with_enqueued_unbonds() { public_key: delegator1.clone(), delegated_amount: U512::from(d1_stake), }]), + reservations: None, }; let mut reader = MockStateReader::new() @@ -1817,6 +1846,7 @@ fn should_slash_a_validator_and_delegator_with_enqueued_unbonds() { public_key: delegator2.clone(), delegated_amount: U512::from(d2_stake), }]), + reservations: None, }, ), ], @@ -2005,6 +2035,7 @@ fn should_handle_unbonding_to_oneself_correctly() { Default::default(), 0, u64::MAX, + 0, ); expected_bid_1.deactivate(); update.assert_written_bid(account1_hash, BidKind::Validator(Box::new(expected_bid_1))); @@ -2153,6 +2184,7 @@ fn should_handle_unbonding_to_a_delegator_correctly() { Default::default(), 0, u64::MAX, + 0, ); expected_bid_1.deactivate(); update.assert_written_bid(account1_hash, BidKind::Validator(Box::new(expected_bid_1))); @@ -2281,6 +2313,7 @@ fn should_handle_legacy_unbonding_to_oneself_correctly() { Default::default(), 0, u64::MAX, + 0, ); expected_bid_1.deactivate(); update.assert_written_bid(account1_hash, BidKind::Validator(Box::new(expected_bid_1))); @@ -2457,6 +2490,7 @@ fn should_handle_legacy_unbonding_to_a_delegator_correctly() { Default::default(), 0, u64::MAX, + 0, ); expected_bid_1.deactivate(); update.assert_written_bid(account1_hash, BidKind::Validator(Box::new(expected_bid_1))); diff --git a/utils/global-state-update-gen/src/utils.rs b/utils/global-state-update-gen/src/utils.rs index 12ed82e4ce..16ecc64506 100644 --- a/utils/global-state-update-gen/src/utils.rs +++ b/utils/global-state-update-gen/src/utils.rs @@ -5,7 +5,7 @@ use std::{ }; use casper_types::{ - bytesrepr::ToBytes, checksummed_hex, system::auction::SeigniorageRecipientsSnapshot, + bytesrepr::ToBytes, checksummed_hex, system::auction::SeigniorageRecipientsSnapshotV2, AsymmetricType, Digest, Key, ProtocolVersion, PublicKey, StoredValue, U512, }; @@ -62,8 +62,8 @@ pub struct ValidatorsDiff { /// Calculates the sets of added and removed validators between the two snapshots. pub fn validators_diff( - old_snapshot: &SeigniorageRecipientsSnapshot, - new_snapshot: &SeigniorageRecipientsSnapshot, + old_snapshot: &SeigniorageRecipientsSnapshotV2, + new_snapshot: &SeigniorageRecipientsSnapshotV2, ) -> ValidatorsDiff { let old_validators: BTreeSet<_> = old_snapshot .values() diff --git a/utils/global-state-update-gen/src/validators.rs b/utils/global-state-update-gen/src/validators.rs index b3db70d084..0d99b70120 100644 --- a/utils/global-state-update-gen/src/validators.rs +++ b/utils/global-state-update-gen/src/validators.rs @@ -45,6 +45,7 @@ pub(crate) fn generate_validators_update(matches: &ArgMatches<'_>) { bonded_amount: stake, delegation_rate: None, delegators: None, + reservations: None, }), } }) diff --git a/utils/validation/src/generators.rs b/utils/validation/src/generators.rs index 66000b2b2f..96ac174b5e 100644 --- a/utils/validation/src/generators.rs +++ b/utils/validation/src/generators.rs @@ -123,6 +123,7 @@ pub fn make_abi_test_fixtures() -> Result { u64::MAX, 0, u64::MAX, + 0, ); let validator_bid_kind = BidKind::Validator(Box::new(validator_bid)); let delegator_public_key = PublicKey::from(&delegator_secret_key);