diff --git a/Makefile b/Makefile index 1ed27399f1..52384b96b1 100644 --- a/Makefile +++ b/Makefile @@ -131,7 +131,7 @@ lint-smart-contracts: .PHONY: audit-rs audit-rs: - $(CARGO) audit --ignore RUSTSEC-2024-0437 --ignore RUSTSEC-2025-0022 --ignore RUSTSEC-2025-0055 --ignore RUSTSEC-2026-0001 + $(CARGO) audit --ignore RUSTSEC-2024-0437 --ignore RUSTSEC-2025-0022 --ignore RUSTSEC-2025-0055 --ignore RUSTSEC-2026-0001 --ignore RUSTSEC-2026-0007 .PHONY: audit audit: audit-rs diff --git a/execution_engine/src/engine_state/engine_config.rs b/execution_engine/src/engine_state/engine_config.rs index f10318beb6..fb3352ab22 100644 --- a/execution_engine/src/engine_state/engine_config.rs +++ b/execution_engine/src/engine_state/engine_config.rs @@ -7,9 +7,9 @@ use num_rational::Ratio; use num_traits::One; use casper_types::{ - account::AccountHash, FeeHandling, ProtocolVersion, PublicKey, RefundHandling, StorageCosts, - SystemConfig, TimeDiff, WasmConfig, DEFAULT_FEE_HANDLING, DEFAULT_MINIMUM_BID_AMOUNT, - DEFAULT_REFUND_HANDLING, + account::AccountHash, FeeHandling, ProtocolVersion, PublicKey, RefundHandling, RewardsHandling, + StorageCosts, SystemConfig, TimeDiff, WasmConfig, DEFAULT_FEE_HANDLING, + DEFAULT_MINIMUM_BID_AMOUNT, DEFAULT_REFUND_HANDLING, }; /// Default value for a maximum query depth configuration option. @@ -93,6 +93,7 @@ pub struct EngineConfig { pub(crate) compute_rewards: bool, pub(crate) enable_entity: bool, pub(crate) trap_on_ambiguous_entity_version: bool, + pub(crate) rewards_handling: RewardsHandling, storage_costs: StorageCosts, } @@ -118,6 +119,7 @@ impl Default for EngineConfig { protocol_version: DEFAULT_PROTOCOL_VERSION, enable_entity: DEFAULT_ENABLE_ENTITY, trap_on_ambiguous_entity_version: DEFAULT_TRAP_ON_AMBIGUOUS_ENTITY_VERSION, + rewards_handling: RewardsHandling::Standard, storage_costs: Default::default(), } } @@ -224,6 +226,11 @@ impl EngineConfig { self.trap_on_ambiguous_entity_version } + /// Returns the current configuration for rewards handling. + pub fn rewards_handling(&self) -> RewardsHandling { + self.rewards_handling.clone() + } + /// Sets the protocol version of the config. /// /// NOTE: This is only useful to the WasmTestBuilder for emulating a network upgrade, and hence @@ -267,6 +274,7 @@ pub struct EngineConfigBuilder { balance_hold_interval: Option, enable_entity: Option, trap_on_ambiguous_entity_version: Option, + rewards_handling: Option, storage_costs: Option, } @@ -487,6 +495,7 @@ impl EngineConfigBuilder { .trap_on_ambiguous_entity_version .unwrap_or(DEFAULT_TRAP_ON_AMBIGUOUS_ENTITY_VERSION); let storage_costs = self.storage_costs.unwrap_or_default(); + let rewards_handling = self.rewards_handling.unwrap_or(RewardsHandling::Standard); EngineConfig { max_associated_keys, @@ -508,6 +517,7 @@ impl EngineConfigBuilder { compute_rewards, enable_entity, trap_on_ambiguous_entity_version, + rewards_handling, storage_costs, } } diff --git a/execution_engine/src/runtime/mod.rs b/execution_engine/src/runtime/mod.rs index b8f9a3c535..a594dbe080 100644 --- a/execution_engine/src/runtime/mod.rs +++ b/execution_engine/src/runtime/mod.rs @@ -53,15 +53,18 @@ use casper_types::{ system::{ self, auction::{self, DelegatorKind, EraInfo}, - handle_payment, mint, CallStackElement, Caller, CallerInfo, SystemEntityType, AUCTION, - HANDLE_PAYMENT, MINT, STANDARD_PAYMENT, + handle_payment, mint, + mint::MINT_SUSTAIN_PURSE_KEY, + CallStackElement, Caller, CallerInfo, SystemEntityType, AUCTION, HANDLE_PAYMENT, MINT, + STANDARD_PAYMENT, }, AccessRights, ApiError, BlockGlobalAddr, BlockTime, ByteCode, ByteCodeAddr, ByteCodeHash, ByteCodeKind, CLTyped, CLValue, ContextAccessRights, Contract, ContractWasm, EntityAddr, EntityKind, EntityVersion, EntityVersionKey, EntityVersions, Gas, GrantedAccess, Group, Groups, HashAddr, HostFunction, HostFunctionCost, InitiatorAddr, Key, NamedArg, Package, PackageHash, - PackageStatus, Phase, PublicKey, RuntimeArgs, RuntimeFootprint, StoredValue, Transfer, - TransferResult, TransferV2, TransferredTo, URef, DICTIONARY_ITEM_KEY_MAX_LENGTH, U512, + PackageStatus, Phase, PublicKey, RewardsHandling, RuntimeArgs, RuntimeFootprint, StoredValue, + Transfer, TransferResult, TransferV2, TransferredTo, URef, DICTIONARY_ITEM_KEY_MAX_LENGTH, + U512, }; use crate::{ @@ -1254,8 +1257,33 @@ where // ExecError>` auction::METHOD_DISTRIBUTE => (|| { runtime.charge_system_contract_call(auction_costs.distribute)?; + let rewards_handling = self.context().engine_config().rewards_handling(); let rewards = Self::get_named_argument(runtime_args, auction::ARG_REWARDS_MAP)?; - runtime.distribute(rewards).map_err(Self::reverter)?; + + let sustain_purse = match rewards_handling { + RewardsHandling::Standard => None, + RewardsHandling::Sustain { .. } => { + let sustain_purse = { + let mint_hash = self.context.get_system_contract(AUCTION)?; + match self + .context + .state() + .borrow_mut() + .get_named_keys(EntityAddr::System(mint_hash.value()))? + .get(MINT_SUSTAIN_PURSE_KEY) + { + Some(Key::URef(uref)) => Some(*uref), + Some(_) | None => None, + } + }; + + sustain_purse + } + }; + + runtime + .distribute(rewards, sustain_purse, rewards_handling) + .map_err(Self::reverter)?; CLValue::from_t(()).map_err(Self::reverter) })(), diff --git a/execution_engine_testing/test_support/src/genesis_config_builder.rs b/execution_engine_testing/test_support/src/genesis_config_builder.rs index 31572d9c9c..39afa47e5f 100644 --- a/execution_engine_testing/test_support/src/genesis_config_builder.rs +++ b/execution_engine_testing/test_support/src/genesis_config_builder.rs @@ -29,6 +29,7 @@ pub struct GenesisConfigBuilder { gas_hold_balance_handling: Option, gas_hold_interval_millis: Option, enable_addressable_entity: Option, + rewards_ratio: Option>, storage_costs: Option, } @@ -98,6 +99,12 @@ impl GenesisConfigBuilder { self } + /// Sets the rewards ratio. + pub fn with_rewards_ratio(mut self, rewards_ratio: Ratio) -> Self { + self.rewards_ratio = Some(rewards_ratio); + self + } + /// Sets the storage_costs handling. pub fn with_storage_costs(mut self, storage_costs: StorageCosts) -> Self { self.storage_costs = Some(storage_costs); @@ -125,6 +132,7 @@ impl GenesisConfigBuilder { .unwrap_or(DEFAULT_GAS_HOLD_INTERVAL_MILLIS), self.enable_addressable_entity .unwrap_or(DEFAULT_ENABLE_ENTITY), + self.rewards_ratio, self.storage_costs.unwrap_or_default(), ) } diff --git a/execution_engine_testing/test_support/src/lib.rs b/execution_engine_testing/test_support/src/lib.rs index 96106522c7..fbaeb6835b 100644 --- a/execution_engine_testing/test_support/src/lib.rs +++ b/execution_engine_testing/test_support/src/lib.rs @@ -116,6 +116,13 @@ pub static DEFAULT_PROPOSER_PUBLIC_KEY: Lazy = Lazy::new(|| { /// Default proposer address. pub static DEFAULT_PROPOSER_ADDR: Lazy = Lazy::new(|| AccountHash::from(&*DEFAULT_PROPOSER_PUBLIC_KEY)); + +/// Default public key to associate with the sustain purse. +pub static DEFAULT_SUSTAIN_PUBLIC_KEY: Lazy = Lazy::new(|| { + let secret_key = SecretKey::ed25519_from_bytes([207; SecretKey::ED25519_LENGTH]).unwrap(); + PublicKey::from(&secret_key) +}); + /// Default accounts. pub static DEFAULT_ACCOUNTS: Lazy> = Lazy::new(|| { let mut ret = Vec::new(); diff --git a/execution_engine_testing/test_support/src/transfer_request_builder.rs b/execution_engine_testing/test_support/src/transfer_request_builder.rs index 4ca63ced5b..1e480509e0 100644 --- a/execution_engine_testing/test_support/src/transfer_request_builder.rs +++ b/execution_engine_testing/test_support/src/transfer_request_builder.rs @@ -19,7 +19,7 @@ use casper_types::{ bytesrepr::ToBytes, system::mint::{ARG_AMOUNT, ARG_ID, ARG_SOURCE, ARG_TARGET}, BlockTime, CLValue, Digest, FeeHandling, Gas, InitiatorAddr, ProtocolVersion, RefundHandling, - RuntimeArgs, TransactionHash, TransactionV1Hash, TransferTarget, URef, + RewardsHandling, RuntimeArgs, TransactionHash, TransactionV1Hash, TransferTarget, URef, DEFAULT_GAS_HOLD_INTERVAL, U512, }; @@ -61,6 +61,7 @@ impl TransferRequestBuilder { Ratio::new_raw(U512::zero(), U512::zero()), DEFAULT_ENABLE_ENTITY, 2_500_000_000, + RewardsHandling::Standard, ); /// The default value used for `TransferRequest::state_hash`. pub const DEFAULT_STATE_HASH: Digest = Digest::from_raw([1; 32]); diff --git a/execution_engine_testing/test_support/src/upgrade_request_builder.rs b/execution_engine_testing/test_support/src/upgrade_request_builder.rs index 207ecc6c3c..2ab40a1366 100644 --- a/execution_engine_testing/test_support/src/upgrade_request_builder.rs +++ b/execution_engine_testing/test_support/src/upgrade_request_builder.rs @@ -4,7 +4,7 @@ use num_rational::Ratio; use casper_types::{ ChainspecRegistry, Digest, EraId, FeeHandling, HoldBalanceHandling, Key, ProtocolUpgradeConfig, - ProtocolVersion, StoredValue, + ProtocolVersion, RewardsHandling, StoredValue, }; /// Builds an `UpgradeConfig`. @@ -27,6 +27,7 @@ pub struct UpgradeRequestBuilder { maximum_delegation_amount: u64, minimum_delegation_amount: u64, enable_addressable_entity: bool, + rewards_handling: RewardsHandling, } impl UpgradeRequestBuilder { @@ -170,6 +171,7 @@ impl UpgradeRequestBuilder { self.maximum_delegation_amount, self.minimum_delegation_amount, self.enable_addressable_entity, + self.rewards_handling, ) } } @@ -195,6 +197,7 @@ impl Default for UpgradeRequestBuilder { maximum_delegation_amount: u64::MAX, minimum_delegation_amount: 0, enable_addressable_entity: false, + rewards_handling: RewardsHandling::Standard, } } } diff --git a/execution_engine_testing/test_support/src/wasm_test_builder.rs b/execution_engine_testing/test_support/src/wasm_test_builder.rs index f5ceb1e972..7d2483340e 100644 --- a/execution_engine_testing/test_support/src/wasm_test_builder.rs +++ b/execution_engine_testing/test_support/src/wasm_test_builder.rs @@ -67,8 +67,8 @@ use casper_types::{ BlockTime, ByteCode, ByteCodeAddr, ByteCodeHash, CLTyped, CLValue, Contract, Digest, EntityAddr, EntryPoints, EraId, FeeHandling, Gas, HandlePaymentCosts, HoldBalanceHandling, InitiatorAddr, Key, KeyTag, MintCosts, Motes, Package, PackageHash, Phase, - ProtocolUpgradeConfig, ProtocolVersion, PublicKey, RefundHandling, StoredValue, - SystemHashRegistry, TransactionHash, TransactionV1Hash, URef, OS_PAGE_SIZE, U512, + ProtocolUpgradeConfig, ProtocolVersion, PublicKey, RefundHandling, RewardsHandling, + StoredValue, SystemHashRegistry, TransactionHash, TransactionV1Hash, URef, OS_PAGE_SIZE, U512, }; use crate::{ @@ -848,6 +848,7 @@ where credit_cap, enable_addressable_entity, config.system_costs_config.mint_costs().transfer, + config.core_config.rewards_handling.clone(), ); let bidding_req = BiddingRequest::new( @@ -1018,6 +1019,7 @@ where credit_cap, self.chainspec.core_config.enable_addressable_entity, self.chainspec.system_costs_config.mint_costs().transfer, + self.chainspec.core_config.rewards_handling.clone(), ) } @@ -1080,6 +1082,71 @@ where distribute_block_rewards_result } + /// Distributes the rewards. + pub fn distribute_with_rewards_handling( + &mut self, + pre_state_hash: Option, + protocol_version: ProtocolVersion, + rewards: BTreeMap>, + block_time: u64, + rewards_handling: RewardsHandling, + ) -> BlockRewardsResult { + let pre_state_hash = pre_state_hash.or(self.post_state_hash).unwrap(); + let administrators: BTreeSet = self + .chainspec + .core_config + .administrators + .iter() + .map(|x| x.to_account_hash()) + .collect(); + let allow_unrestricted = self.chainspec.core_config.allow_unrestricted_transfers; + let transfer_config = TransferConfig::new(administrators, allow_unrestricted); + let include_credits = self.chainspec.core_config.fee_handling == FeeHandling::NoFee; + let credit_cap = Ratio::new_raw( + U512::from(*self.chainspec.core_config.validator_credit_cap.numer()), + U512::from(*self.chainspec.core_config.validator_credit_cap.denom()), + ); + + let native_runtime_config = NativeRuntimeConfig::new( + transfer_config, + self.chainspec.core_config.fee_handling, + self.chainspec.core_config.refund_handling, + self.chainspec.core_config.vesting_schedule_period.millis(), + self.chainspec.core_config.allow_auction_bids, + self.chainspec.core_config.compute_rewards, + self.chainspec.core_config.max_delegators_per_validator, + self.chainspec.core_config.minimum_bid_amount, + self.chainspec.core_config.minimum_delegation_amount, + self.chainspec.core_config.maximum_delegation_amount, + self.chainspec.core_config.gas_hold_interval.millis(), + include_credits, + credit_cap, + self.chainspec.core_config.enable_addressable_entity, + self.chainspec.system_costs_config.mint_costs().transfer, + rewards_handling, + ); + + let distribute_req = BlockRewardsRequest::new( + native_runtime_config, + pre_state_hash, + protocol_version, + BlockTime::new(block_time), + rewards, + ); + let distribute_block_rewards_result = self + .data_access_layer + .distribute_block_rewards(distribute_req); + + if let BlockRewardsResult::Success { + post_state_hash, .. + } = distribute_block_rewards_result + { + self.post_state_hash = Some(post_state_hash); + } + + distribute_block_rewards_result + } + /// Finalizes payment for a transaction pub fn handle_fee( &mut self, diff --git a/execution_engine_testing/tests/src/test/explorer/faucet.rs b/execution_engine_testing/tests/src/test/explorer/faucet.rs index 73d03b743c..7d6d8e09cb 100644 --- a/execution_engine_testing/tests/src/test/explorer/faucet.rs +++ b/execution_engine_testing/tests/src/test/explorer/faucet.rs @@ -663,14 +663,14 @@ fn faucet_costs() { // This test will fail if execution costs vary. The expected costs should not be updated // without understanding why the cost has changed. If the costs do change, it should be // reflected in the "Costs by Entry Point" section of the faucet crate's README.md. - const EXPECTED_FAUCET_INSTALL_COST: u64 = 160_503_972_212; + const EXPECTED_FAUCET_INSTALL_COST: u64 = 149_263_295_166; const EXPECTED_FAUCET_INSTALL_COST_ALT: u64 = 149_230_872_143; - const EXPECTED_FAUCET_SET_VARIABLES_COST: u64 = 79_455_975; + const EXPECTED_FAUCET_SET_VARIABLES_COST: u64 = 79_463_750; - const EXPECTED_FAUCET_CALL_BY_INSTALLER_COST: u64 = 2_652_626_533; + const EXPECTED_FAUCET_CALL_BY_INSTALLER_COST: u64 = 2_652_633_308; - const EXPECTED_FAUCET_CALL_BY_USER_COST: u64 = 2_558_318_531; + const EXPECTED_FAUCET_CALL_BY_USER_COST: u64 = 2_558_333_326; let installer_account = AccountHash::new([1u8; 32]); let user_account: AccountHash = AccountHash::new([2u8; 32]); 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 2010dd0c4f..310cd50653 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 @@ -11,8 +11,8 @@ use casper_engine_test_support::{ ExecuteRequestBuilder, LmdbWasmTestBuilder, StepRequestBuilder, UpgradeRequestBuilder, DEFAULT_ACCOUNT_ADDR, DEFAULT_GENESIS_TIMESTAMP_MILLIS, DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS, DEFAULT_MINIMUM_DELEGATION_AMOUNT, DEFAULT_PROTOCOL_VERSION, DEFAULT_ROUND_SEIGNIORAGE_RATE, - LOCAL_GENESIS_REQUEST, MINIMUM_ACCOUNT_CREATION_BALANCE, PRODUCTION_ROUND_SEIGNIORAGE_RATE, - SYSTEM_ADDR, TIMESTAMP_MILLIS_INCREMENT, + DEFAULT_SUSTAIN_PUBLIC_KEY, LOCAL_GENESIS_REQUEST, MINIMUM_ACCOUNT_CREATION_BALANCE, + PRODUCTION_ROUND_SEIGNIORAGE_RATE, SYSTEM_ADDR, TIMESTAMP_MILLIS_INCREMENT, }; use casper_storage::data_access_layer::AuctionMethod; use casper_types::{ @@ -25,8 +25,8 @@ use casper_types::{ ARG_DELEGATOR, ARG_PUBLIC_KEY, ARG_REWARDS_MAP, ARG_VALIDATOR, DELEGATION_RATE_DENOMINATOR, METHOD_DISTRIBUTE, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, }, - EntityAddr, EraId, ProtocolVersion, PublicKey, SecretKey, Timestamp, - DEFAULT_MINIMUM_BID_AMOUNT, U512, + EntityAddr, EraId, GenesisAccount, ProtocolVersion, PublicKey, RewardsHandling, SecretKey, + Timestamp, DEFAULT_MINIMUM_BID_AMOUNT, U512, }; const ARG_ENTRY_POINT: &str = "entry_point"; @@ -1673,6 +1673,276 @@ fn should_distribute_uneven_delegation_rate_zero() { )); } +#[ignore] +#[test] +fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { + const VALIDATOR_1_STAKE: u64 = 200_000_000_000; + const DELEGATOR_1_STAKE: u64 = 600_000_000_000; + const DELEGATOR_2_STAKE: u64 = 10_000_000_000_000; + const TOTAL_DELEGATOR_STAKE: u64 = DELEGATOR_1_STAKE + DELEGATOR_2_STAKE; + const TOTAL_STAKE: u64 = VALIDATOR_1_STAKE + TOTAL_DELEGATOR_STAKE; + + const VALIDATOR_1_DELEGATION_RATE: DelegationRate = 0; + + let system_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *SYSTEM_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let validator_1_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *VALIDATOR_1_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let delegator_1_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *DELEGATOR_1_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let delegator_2_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *DELEGATOR_2_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let validator_1_add_bid_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_AMOUNT => U512::from(VALIDATOR_1_STAKE), + ARG_DELEGATION_RATE => VALIDATOR_1_DELEGATION_RATE, + ARG_PUBLIC_KEY => VALIDATOR_1.clone(), + }, + ) + .build(); + + let delegator_1_delegate_request = ExecuteRequestBuilder::standard( + *DELEGATOR_1_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DELEGATOR_1_STAKE), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_1.clone(), + }, + ) + .build(); + + let delegator_2_delegate_request = ExecuteRequestBuilder::standard( + *DELEGATOR_2_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DELEGATOR_2_STAKE), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_2.clone(), + }, + ) + .build(); + + let post_genesis_requests = vec![ + system_fund_request, + validator_1_fund_request, + delegator_1_fund_request, + delegator_2_fund_request, + validator_1_add_bid_request, + delegator_1_delegate_request, + delegator_2_delegate_request, + ]; + + let mut builder = LmdbWasmTestBuilder::default(); + + let mut default_request = LOCAL_GENESIS_REQUEST.clone(); + default_request.push_genesis_account(GenesisAccount::SustainAccount { + public_key: DEFAULT_SUSTAIN_PUBLIC_KEY.clone(), + }); + default_request.push_rewards_ratio(Ratio::new(1, 4)); + builder.run_genesis(default_request); + + let protocol_version = DEFAULT_PROTOCOL_VERSION; + // initial token supply + let initial_supply = builder.total_supply(protocol_version, None); + let total_payout = builder.base_round_reward(None, protocol_version); + let expected_total_reward = *GENESIS_ROUND_SEIGNIORAGE_RATE * initial_supply; + let expected_total_reward_integer = expected_total_reward.to_integer(); + assert_eq!(total_payout, expected_total_reward_integer); + + let sustain_ratio_as_u512 = Ratio::new(U512::from(1), U512::from(4)); + + let expected_total_reward = + expected_total_reward * { Ratio::new(U512::one(), U512::one()) - sustain_ratio_as_u512 }; + + for request in post_genesis_requests { + builder.exec(request).commit().expect_success(); + } + + 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 block_rewards = { + let mut ret = BTreeMap::new(); + ret.insert(VALIDATOR_1.clone(), vec![total_payout]); + ret + }; + + let sustain_purse = builder + .get_account(DEFAULT_SUSTAIN_PUBLIC_KEY.to_account_hash()) + .expect("must have sustain account as part of genesis setup") + .main_purse(); + + let block_rewards_result = builder.distribute_with_rewards_handling( + None, + ProtocolVersion::V2_0_0, + block_rewards.clone(), + 0, + RewardsHandling::Sustain { + ratio: Ratio::new(1, 4), + purse_address: sustain_purse.to_formatted_string(), + }, + ); + assert!(block_rewards_result.is_success()); + + let sustain_purse_expected_balance = { + let total = { + let mut ret = U512::zero(); + for rewards_vec in block_rewards.values() { + for reward in rewards_vec { + ret += *reward + } + } + + ret + }; + + Ratio::new(total, U512::one()) * sustain_ratio_as_u512 + } + .to_integer(); + + let actual_sustain_balance = builder.get_purse_balance(sustain_purse); + assert_eq!(actual_sustain_balance, sustain_purse_expected_balance); + + let delegators_share = { + let commission_rate = Ratio::new( + U512::from(VALIDATOR_1_DELEGATION_RATE), + U512::from(DELEGATION_RATE_DENOMINATOR), + ); + let reward_multiplier = + Ratio::new(U512::from(TOTAL_DELEGATOR_STAKE), U512::from(TOTAL_STAKE)); + let delegator_reward = expected_total_reward + .checked_mul(&reward_multiplier) + .expect("must get delegator reward"); + let commission = delegator_reward + .checked_mul(&commission_rate) + .expect("must get commission"); + delegator_reward.checked_sub(&commission).unwrap() + }; + + let delegator_1_expected_payout = { + let reward_multiplier = Ratio::new( + U512::from(DELEGATOR_1_STAKE), + U512::from(TOTAL_DELEGATOR_STAKE), + ); + delegators_share + .checked_mul(&reward_multiplier) + .map(|ratio| ratio.to_integer()) + .unwrap() + }; + + let delegator_2_expected_payout = { + let reward_multiplier = Ratio::new( + U512::from(DELEGATOR_2_STAKE), + U512::from(TOTAL_DELEGATOR_STAKE), + ); + delegators_share + .checked_mul(&reward_multiplier) + .map(|ratio| ratio.to_integer()) + .unwrap() + }; + + let validator_1_expected_payout = { + let total_delegator_payout = delegator_1_expected_payout + delegator_2_expected_payout; + + let validators_part = expected_total_reward - total_delegator_payout; + validators_part.to_integer() + }; + + let validator_1_updated_stake = { + let validator_stake_before = U512::from(VALIDATOR_1_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_updated_stake, validator_1_expected_payout); + + let delegator_1_updated_stake = { + let delegator_stake_before = U512::from(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_updated_stake, delegator_1_expected_payout); + + let delegator_2_updated_stake = { + let delegator_stake_before = U512::from(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_updated_stake, delegator_2_expected_payout); + + let era_info = get_era_info(&mut builder); + + assert!(matches!( + era_info.select(VALIDATOR_1.clone()).next(), + Some(SeigniorageAllocation::Validator { validator_public_key, amount }) + if *validator_public_key == *VALIDATOR_1 && *amount == validator_1_updated_stake + )); + + assert!(matches!( + era_info.select(DELEGATOR_1.clone()).next(), + Some(SeigniorageAllocation::DelegatorKind { delegator_kind: DelegatorKind::PublicKey(delegator_public_key), amount, .. }) + if *delegator_public_key == *DELEGATOR_1 && *amount == delegator_1_updated_stake + )); + + assert!(matches!( + era_info.select(DELEGATOR_2.clone()).next(), + Some(SeigniorageAllocation::DelegatorKind { delegator_kind: DelegatorKind::PublicKey(delegator_public_key), amount, .. }) + if *delegator_public_key == *DELEGATOR_2 && *amount == delegator_2_updated_stake + )); +} + #[ignore] #[test] fn should_distribute_with_multiple_validators_and_delegators() { @@ -2009,6 +2279,375 @@ fn should_distribute_with_multiple_validators_and_delegators() { )); } +#[ignore] +#[test] +fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_on() { + const VALIDATOR_1_STAKE: u64 = 1_000_000_000_000; + const VALIDATOR_2_STAKE: u64 = 1_000_000_000_000; + const VALIDATOR_3_STAKE: u64 = 1_000_000_000_000; + + const DELEGATION_RATE: DelegationRate = DELEGATION_RATE_DENOMINATOR / 2; + + const DELEGATOR_1_STAKE: u64 = 1_000_000_000_000; + + let system_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *SYSTEM_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let validator_1_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *VALIDATOR_1_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let validator_2_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *VALIDATOR_2_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let validator_3_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *VALIDATOR_3_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let delegator_1_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *DELEGATOR_1_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let delegator_2_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *DELEGATOR_2_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let delegator_3_fund_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + ARG_TARGET => *DELEGATOR_3_ADDR, + ARG_AMOUNT => U512::from(TRANSFER_AMOUNT) + }, + ) + .build(); + + let validator_1_add_bid_request = ExecuteRequestBuilder::standard( + *VALIDATOR_1_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_AMOUNT => U512::from(VALIDATOR_1_STAKE), + ARG_DELEGATION_RATE => DELEGATION_RATE, + ARG_PUBLIC_KEY => VALIDATOR_1.clone(), + }, + ) + .build(); + + let validator_2_add_bid_request = ExecuteRequestBuilder::standard( + *VALIDATOR_2_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_AMOUNT => U512::from(VALIDATOR_2_STAKE), + ARG_DELEGATION_RATE => DELEGATION_RATE, + ARG_PUBLIC_KEY => VALIDATOR_2.clone(), + }, + ) + .build(); + + let validator_3_add_bid_request = ExecuteRequestBuilder::standard( + *VALIDATOR_3_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_AMOUNT => U512::from(VALIDATOR_3_STAKE), + ARG_DELEGATION_RATE => DELEGATION_RATE, + ARG_PUBLIC_KEY => VALIDATOR_3.clone(), + }, + ) + .build(); + + let delegator_1_validator_1_delegate_request = ExecuteRequestBuilder::standard( + *DELEGATOR_1_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DELEGATOR_1_STAKE), + ARG_VALIDATOR => VALIDATOR_1.clone(), + ARG_DELEGATOR => DELEGATOR_1.clone(), + }, + ) + .build(); + + let delegator_1_validator_2_delegate_request = ExecuteRequestBuilder::standard( + *DELEGATOR_1_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DELEGATOR_1_STAKE), + ARG_VALIDATOR => VALIDATOR_2.clone(), + ARG_DELEGATOR => DELEGATOR_1.clone(), + }, + ) + .build(); + + let delegator_1_validator_3_delegate_request = ExecuteRequestBuilder::standard( + *DELEGATOR_1_ADDR, + CONTRACT_DELEGATE, + runtime_args! { + ARG_AMOUNT => U512::from(DELEGATOR_1_STAKE), + ARG_VALIDATOR => VALIDATOR_3.clone(), + ARG_DELEGATOR => DELEGATOR_1.clone(), + }, + ) + .build(); + + let post_genesis_requests = vec![ + system_fund_request, + validator_1_fund_request, + validator_2_fund_request, + validator_3_fund_request, + delegator_1_fund_request, + delegator_2_fund_request, + delegator_3_fund_request, + validator_1_add_bid_request, + validator_2_add_bid_request, + validator_3_add_bid_request, + delegator_1_validator_1_delegate_request, + delegator_1_validator_2_delegate_request, + delegator_1_validator_3_delegate_request, + ]; + + let mut builder = LmdbWasmTestBuilder::default(); + + let mut default_request = LOCAL_GENESIS_REQUEST.clone(); + default_request.push_genesis_account(GenesisAccount::SustainAccount { + public_key: DEFAULT_SUSTAIN_PUBLIC_KEY.clone(), + }); + default_request.push_rewards_ratio(Ratio::new(1, 4)); + builder.run_genesis(default_request); + + let protocol_version = DEFAULT_PROTOCOL_VERSION; + // initial token supply + let initial_supply = builder.total_supply(protocol_version, None); + let total_payout = builder.base_round_reward(None, protocol_version); + let expected_total_reward = *GENESIS_ROUND_SEIGNIORAGE_RATE * initial_supply; + let expected_total_reward_integer = expected_total_reward.to_integer(); + assert_eq!(total_payout, expected_total_reward_integer); + + let sustain_ratio_as_u512 = Ratio::new(U512::from(1), U512::from(4)); + + let expected_total_reward = + expected_total_reward * { Ratio::new(U512::one(), U512::one()) - sustain_ratio_as_u512 }; + let expected_total_reward_integer = expected_total_reward.to_integer(); + + for request in post_genesis_requests { + builder.exec(request).commit().expect_success(); + } + + 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]); + rewards.insert(VALIDATOR_2.clone(), vec![total_payout]); + rewards.insert(VALIDATOR_3.clone(), vec![total_payout]); + + let sustain_purse = builder + .get_account(DEFAULT_SUSTAIN_PUBLIC_KEY.to_account_hash()) + .expect("must have sustain account as part of genesis setup") + .main_purse(); + + let block_rewards_result = builder.distribute_with_rewards_handling( + None, + ProtocolVersion::V2_0_0, + rewards.clone(), + 0, + RewardsHandling::Sustain { + ratio: Ratio::new(1, 4), + purse_address: sustain_purse.to_formatted_string(), + }, + ); + assert!(block_rewards_result.is_success()); + + let validator_1_delegator_1_share = { + let total_reward = &Ratio::from(expected_total_reward_integer); + + let validator_1_total_stake = VALIDATOR_1_STAKE + DELEGATOR_1_STAKE; + + let delegator_total_stake = U512::from(DELEGATOR_1_STAKE); + let commission_rate = Ratio::new( + U512::from(DELEGATION_RATE), + U512::from(DELEGATION_RATE_DENOMINATOR), + ); + let reward_multiplier = + Ratio::new(delegator_total_stake, U512::from(validator_1_total_stake)); + let delegator_reward = total_reward + .checked_mul(&reward_multiplier) + .expect("must get delegator reward"); + let commission = delegator_reward + .checked_mul(&commission_rate) + .expect("must get commission"); + delegator_reward.checked_sub(&commission).unwrap() + } + .to_integer(); + + let validator_1_actual_payout = { + let validator_balance_before = U512::from(VALIDATOR_1_STAKE); + let validator_balance_after = get_validator_bid(&mut builder, VALIDATOR_1.clone()) + .expect("should have validator bid") + .staked_amount(); + validator_balance_after - validator_balance_before + }; + + let validator_1_expected_payout = { + let validator_share = expected_total_reward; + let validator_portion = validator_share - Ratio::from(validator_1_delegator_1_share); + validator_portion.to_integer() + }; + assert_eq!(validator_1_actual_payout, validator_1_expected_payout); + + let validator_2_delegator_1_share = { + let validator_2_total_stake = VALIDATOR_2_STAKE + DELEGATOR_1_STAKE; + + let total_reward = &Ratio::from(expected_total_reward.to_integer()); + + let delegator_total_stake = U512::from(DELEGATOR_1_STAKE); + let commission_rate = Ratio::new( + U512::from(DELEGATION_RATE), + U512::from(DELEGATION_RATE_DENOMINATOR), + ); + let reward_multiplier = + Ratio::new(delegator_total_stake, U512::from(validator_2_total_stake)); + let delegator_reward = total_reward + .checked_mul(&reward_multiplier) + .expect("must get delegator reward"); + let commission = delegator_reward + .checked_mul(&commission_rate) + .expect("must get commission"); + delegator_reward.checked_sub(&commission).unwrap() + } + .to_integer(); + + let validator_2_actual_payout = { + let validator_balance_before = U512::from(VALIDATOR_2_STAKE); + let validator_balance_after = get_validator_bid(&mut builder, VALIDATOR_2.clone()) + .expect("should have validator bid") + .staked_amount(); + validator_balance_after - validator_balance_before + }; + let validator_2_expected_payout = { + let validator_share = expected_total_reward; + let validator_portion = validator_share - Ratio::from(validator_2_delegator_1_share); + validator_portion.to_integer() + }; + assert_eq!(validator_2_actual_payout, validator_2_expected_payout); + + let validator_3_delegator_1_share = { + let validator_3_total_stake = VALIDATOR_3_STAKE + DELEGATOR_1_STAKE; + + let total_reward = &Ratio::from(expected_total_reward.to_integer()); + + let delegator_total_stake = U512::from(DELEGATOR_1_STAKE); + let commission_rate = Ratio::new( + U512::from(DELEGATION_RATE), + U512::from(DELEGATION_RATE_DENOMINATOR), + ); + let reward_multiplier = + Ratio::new(delegator_total_stake, U512::from(validator_3_total_stake)); + let delegator_reward = total_reward + .checked_mul(&reward_multiplier) + .expect("must get delegator reward"); + let commission = delegator_reward + .checked_mul(&commission_rate) + .expect("must get commission"); + delegator_reward.checked_sub(&commission).unwrap() + } + .to_integer(); + + let validator_3_actual_payout = { + let validator_balance_before = U512::from(VALIDATOR_3_STAKE); + let validator_balance_after = get_validator_bid(&mut builder, VALIDATOR_3.clone()) + .expect("should have validator bid") + .staked_amount(); + validator_balance_after - validator_balance_before + }; + let validator_3_expected_payout = { + let validator_share = expected_total_reward; + let validator_portion = validator_share - Ratio::from(validator_3_delegator_1_share); + validator_portion.to_integer() + }; + assert_eq!(validator_3_actual_payout, validator_3_expected_payout); + + let delegator_1_validator_1_updated_stake = { + let delegator_balance_before = U512::from(DELEGATOR_1_STAKE); + let delegator_balance_after = + get_delegator_staked_amount(&mut builder, VALIDATOR_1.clone(), DELEGATOR_1.clone()); + delegator_balance_after - delegator_balance_before + }; + + assert_eq!( + delegator_1_validator_1_updated_stake, + validator_1_delegator_1_share + ); + + let delegator_1_validator_2_updated_stake = { + let delegator_stake_before = U512::from(DELEGATOR_1_STAKE); + let delegator_stake_after = + get_delegator_staked_amount(&mut builder, VALIDATOR_2.clone(), DELEGATOR_1.clone()); + delegator_stake_after - delegator_stake_before + }; + assert_eq!( + delegator_1_validator_2_updated_stake, + validator_2_delegator_1_share + ); + + let delegator_1_validator_3_updated_stake = { + let delegator_stake_before = U512::from(DELEGATOR_1_STAKE); + let delegator_stake_after = + get_delegator_staked_amount(&mut builder, VALIDATOR_3.clone(), DELEGATOR_1.clone()); + delegator_stake_after - delegator_stake_before + }; + assert_eq!( + delegator_1_validator_3_updated_stake, + validator_3_delegator_1_share + ); +} + #[ignore] #[test] fn should_distribute_with_multiple_validators_and_shared_delegator() { diff --git a/node/src/components/binary_port.rs b/node/src/components/binary_port.rs index 4e8a441bcd..8f6c0dfdb1 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -1185,13 +1185,14 @@ where let snapshot_request = SeigniorageRecipientsRequest::new(*parent_header.state_root_hash()); - let snapshot = match effect_builder + let (snapshot, rewards_ratio) = match effect_builder .get_seigniorage_recipients_snapshot_from_contract_runtime(snapshot_request) .await { SeigniorageRecipientsResult::Success { seigniorage_recipients, - } => seigniorage_recipients, + rewards_ratio, + } => (seigniorage_recipients, rewards_ratio), SeigniorageRecipientsResult::RootNotFound => { return BinaryResponse::new_error(ErrorCode::RootNotFound) } @@ -1238,6 +1239,7 @@ where header.era_id(), validator_rewards, &snapshot, + rewards_ratio, ); match (reward, seigniorage_recipient) { (Ok(Some(reward)), Some(seigniorage_recipient)) => { diff --git a/node/src/reactor/main_reactor.rs b/node/src/reactor/main_reactor.rs index 60b3c27e4d..80678bb7ff 100644 --- a/node/src/reactor/main_reactor.rs +++ b/node/src/reactor/main_reactor.rs @@ -27,7 +27,7 @@ use tracing::{debug, error, info, warn}; use casper_binary_port::{LastProgress, NetworkName, Uptime}; use casper_types::{ - Block, BlockHash, BlockV2, Chainspec, ChainspecRawBytes, EraId, FinalitySignature, + bytesrepr, Block, BlockHash, BlockV2, Chainspec, ChainspecRawBytes, EraId, FinalitySignature, FinalitySignatureV2, PublicKey, TimeDiff, Timestamp, Transaction, U512, }; @@ -1090,6 +1090,15 @@ impl reactor::Reactor for MainReactor { let protocol_version = chainspec.protocol_config.version; let prevent_validator_shutdown = config.value().node.prevent_validator_shutdown; + if !chainspec + .core_config + .rewards_handling + .is_valid_configuration() + { + error!("invalid rewards configuration"); + return Err(Error::BytesRepr(bytesrepr::Error::Formatting)); + } + let trusted_hash = config.value().node.trusted_hash; let (root_dir, config) = config.into_parts(); let (our_secret_key, our_public_key) = config.consensus.load_keys(&root_dir)?; diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index 72d35074f1..25de4dd6b0 100644 --- a/resources/local/chainspec.toml.in +++ b/resources/local/chainspec.toml.in @@ -172,6 +172,7 @@ enable_addressable_entity = false baseline_motes_amount = 2_500_000_000 # Flag on whether ambiguous entity versions returns an execution error. trap_on_ambiguous_entity_version = false +rewards_handling = { type = 'standard'} [highway] # Highway dynamically chooses its round length, between minimum_block_time and maximum_round_length. diff --git a/resources/mainnet/chainspec.toml b/resources/mainnet/chainspec.toml index be0c77ca93..82f58aaf7c 100644 --- a/resources/mainnet/chainspec.toml +++ b/resources/mainnet/chainspec.toml @@ -177,6 +177,7 @@ enable_addressable_entity = false baseline_motes_amount = 2_500_000_000 # Flag on whether ambiguous entity versions returns an execution error. trap_on_ambiguous_entity_version = false +rewards_handling = { type = 'standard' } [highway] # Highway dynamically chooses its round length, between minimum_block_time and maximum_round_length. diff --git a/resources/production/chainspec.toml b/resources/production/chainspec.toml index fe161530a1..5f8694cfe7 100644 --- a/resources/production/chainspec.toml +++ b/resources/production/chainspec.toml @@ -177,6 +177,8 @@ enable_addressable_entity = false baseline_motes_amount = 2_500_000_000 # Flag on whether ambiguous entity versions returns an execution error. trap_on_ambiguous_entity_version = false +rewards_handling = { type = 'standard' } + [highway] # Highway dynamically chooses its round length, between minimum_block_time and maximum_round_length. diff --git a/storage/src/data_access_layer/genesis.rs b/storage/src/data_access_layer/genesis.rs index 5b4f944fb3..c6c94f881a 100644 --- a/storage/src/data_access_layer/genesis.rs +++ b/storage/src/data_access_layer/genesis.rs @@ -1,3 +1,4 @@ +use num_rational::Ratio; #[cfg(test)] use rand::{ distributions::{Distribution, Standard}, @@ -75,6 +76,11 @@ impl GenesisRequest { pub fn chainspec_registry(&self) -> &ChainspecRegistry { &self.chainspec_registry } + + /// Push a rewards ratio into the genesis request. + pub fn push_rewards_ratio(&mut self, rewards_ratio: Ratio) { + self.config.push_rewards_ratio(rewards_ratio) + } } #[cfg(test)] diff --git a/storage/src/data_access_layer/seigniorage_recipients.rs b/storage/src/data_access_layer/seigniorage_recipients.rs index 2117f7d7d1..0e857c84ab 100644 --- a/storage/src/data_access_layer/seigniorage_recipients.rs +++ b/storage/src/data_access_layer/seigniorage_recipients.rs @@ -2,6 +2,7 @@ use crate::tracking_copy::TrackingCopyError; use casper_types::{system::auction::SeigniorageRecipientsSnapshot, Digest}; +use num_rational::Ratio; use std::fmt::{Display, Formatter}; /// Request for seigniorage recipients. @@ -37,6 +38,8 @@ pub enum SeigniorageRecipientsResult { Success { /// Seigniorage recipients. seigniorage_recipients: SeigniorageRecipientsSnapshot, + /// The rewards ratio for the given snapshot + rewards_ratio: Ratio, }, } @@ -55,6 +58,7 @@ impl SeigniorageRecipientsResult { | SeigniorageRecipientsResult::Failure(_) => None, SeigniorageRecipientsResult::Success { seigniorage_recipients, + .. } => Some(seigniorage_recipients), } } diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index 254b918a38..9c280f3b1a 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -31,14 +31,14 @@ use casper_types::{ SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY, }, mint::{ - BalanceHoldAddr, BalanceHoldAddrTag, ARG_AMOUNT, ROUND_SEIGNIORAGE_RATE_KEY, - TOTAL_SUPPLY_KEY, + BalanceHoldAddr, BalanceHoldAddrTag, ARG_AMOUNT, MINT_SUSTAIN_PURSE_KEY, + ROUND_SEIGNIORAGE_RATE_KEY, TOTAL_SUPPLY_KEY, }, AUCTION, HANDLE_PAYMENT, MINT, }, Account, AddressableEntity, BlockGlobalAddr, CLValue, Digest, EntityAddr, EntityEntryPoint, EntryPointAddr, EntryPointValue, HoldsEpoch, Key, KeyTag, Phase, PublicKey, RuntimeArgs, - StoredValue, SystemHashRegistry, U512, + StoredValue, SystemHashRegistry, REWARDS_HANDLING_RATIO_TAG, U512, }; #[cfg(test)] @@ -439,7 +439,19 @@ pub trait CommitProvider: StateProvider { } }; - if let Err(auction_error) = runtime.distribute(rewards.clone()) { + let rewards_handling = request.config().rewards_handling(); + let sustain_purse = match runtime + .runtime_footprint() + .named_keys() + .get(MINT_SUSTAIN_PURSE_KEY) + { + Some(Key::URef(uref)) => Some(*uref), + Some(_) | None => None, + }; + + if let Err(auction_error) = + runtime.distribute(rewards.clone(), sustain_purse, rewards_handling) + { error!( "distribute block rewards failed due to auction error {:?}", auction_error @@ -1050,6 +1062,7 @@ pub trait StateProvider: Send + Sync + Sized { SeigniorageRecipientsResult::AuctionNotFound => EraValidatorsResult::AuctionNotFound, SeigniorageRecipientsResult::Success { seigniorage_recipients, + .. } => { let era_validators = match seigniorage_recipients { SeigniorageRecipientsSnapshot::V1(snapshot) => { @@ -2545,8 +2558,39 @@ fn get_snapshot_data( Err(value) => return value, }; + let query_request = QueryRequest::new(state_hash, Key::RewardsHandling, vec![]); + let rewards_ratio = match state_provider.query(query_request) { + QueryResult::RootNotFound => return SeigniorageRecipientsResult::RootNotFound, + QueryResult::ValueNotFound(_) => Ratio::new(0, 1), + QueryResult::Success { value, .. } => { + if let StoredValue::CLValue(cl_value) = *value { + match cl_value.to_t::>>() { + Ok(rewards_handling) => { + match rewards_handling.get(&REWARDS_HANDLING_RATIO_TAG) { + Some(bytes) => { + let ratio = + match casper_types::bytesrepr::FromBytes::from_bytes(bytes) { + Ok((ratio, _)) => ratio, + Err(_) => Ratio::new(0, 1), + }; + + ratio + } + None => Ratio::new(0, 1), + } + } + Err(_) => Ratio::new(0, 1), + } + } else { + Ratio::new(0, 1) + } + } + QueryResult::Failure(tce) => return SeigniorageRecipientsResult::Failure(tce), + }; + SeigniorageRecipientsResult::Success { seigniorage_recipients: snapshot, + rewards_ratio, } } diff --git a/storage/src/system/auction.rs b/storage/src/system/auction.rs index 3eaaf35e91..ad9bf4ecaf 100644 --- a/storage/src/system/auction.rs +++ b/storage/src/system/auction.rs @@ -23,7 +23,7 @@ use casper_types::{ UnbondEra, UnbondKind, ValidatorBid, ValidatorCredit, ValidatorWeights, DELEGATION_RATE_DENOMINATOR, }, - AccessRights, ApiError, EraId, Key, PublicKey, URef, U512, + AccessRights, ApiError, EraId, Key, PublicKey, RewardsHandling, URef, U512, }; /// Bonding auction contract interface @@ -633,12 +633,48 @@ pub trait Auction: /// according to `reward_factors` returned by the consensus component. // TODO: rework EraInfo and other related structs, methods, etc. to report correct era-end // totals of per-block rewards - fn distribute(&mut self, rewards: BTreeMap>) -> Result<(), Error> { + fn distribute( + &mut self, + rewards: BTreeMap>, + sustain_purse: Option, + rewards_handling: RewardsHandling, + ) -> Result<(), Error> { if self.get_caller() != PublicKey::System.to_account_hash() { error!("invalid caller to auction distribute"); return Err(Error::InvalidCaller); } + let total = { + let mut ret = U512::zero(); + for rewards_vec in rewards.values() { + for reward in rewards_vec { + ret += *reward + } + } + + ret + }; + let total = Ratio::new(total, U512::one()); + let sustain_ratio = match rewards_handling { + RewardsHandling::Standard => Ratio::new(U512::zero(), U512::one()), + RewardsHandling::Sustain { ratio, .. } => { + let numerator = U512::from(*ratio.numer()); + let denom = U512::from(*ratio.denom()); + + Ratio::new(numerator, denom) + } + }; + + let share = (sustain_ratio * total).to_integer(); + + match (rewards_handling, sustain_purse) { + (RewardsHandling::Sustain { .. }, Some(sustain_purse)) => { + self.mint_into_existing_purse(share, sustain_purse)?; + } + (RewardsHandling::Sustain { .. }, None) => return Err(Error::MintReward), + (RewardsHandling::Standard, _) => {} + } + debug!("reading seigniorage recipients snapshot"); let seigniorage_recipients_snapshot = detail::get_seigniorage_recipients_snapshot(self)?; let current_era_id = detail::get_era_id(self)?; @@ -656,6 +692,7 @@ pub trait Auction: current_era_id, &amounts, &SeigniorageRecipientsSnapshot::V2(seigniorage_recipients_snapshot.clone()), + sustain_ratio, ) .map(|infos| infos.into_iter().map(move |info| (proposer.clone(), info))) }) diff --git a/storage/src/system/auction/detail.rs b/storage/src/system/auction/detail.rs index bf099e1293..19a14099ea 100644 --- a/storage/src/system/auction/detail.rs +++ b/storage/src/system/auction/detail.rs @@ -1581,14 +1581,24 @@ pub fn reward( era_id: EraId, rewards: &[U512], seigniorage_recipients_snapshot: &SeigniorageRecipientsSnapshot, + rewards_ratio: Ratio, ) -> Result, Error> { - let validator_rewards = - match rewards_per_validator(validator, era_id, rewards, seigniorage_recipients_snapshot) { - Ok(rewards) => rewards, - Err(Error::ValidatorNotFound) => return Ok(None), - Err(Error::MissingSeigniorageRecipients) => return Ok(None), - Err(err) => return Err(err), - }; + let rewards_ratio_as_u512 = Ratio::new( + U512::from(*rewards_ratio.numer()), + U512::from(*rewards_ratio.denom()), + ); + let validator_rewards = match rewards_per_validator( + validator, + era_id, + rewards, + seigniorage_recipients_snapshot, + rewards_ratio_as_u512, + ) { + Ok(rewards) => rewards, + Err(Error::ValidatorNotFound) => return Ok(None), + Err(Error::MissingSeigniorageRecipients) => return Ok(None), + Err(err) => return Err(err), + }; let reward = validator_rewards .into_iter() @@ -1614,6 +1624,7 @@ pub(crate) fn rewards_per_validator( era_id: EraId, rewards: &[U512], seigniorage_recipients_snapshot: &SeigniorageRecipientsSnapshot, + rewards_ratio: Ratio, ) -> Result, Error> { let mut results = Vec::with_capacity(rewards.len()); @@ -1625,7 +1636,8 @@ pub(crate) fn rewards_per_validator( // record zero allocations for the current validators in EraInfo) .filter(|(amount, eras_back)| !amount.is_zero() || *eras_back == 0) { - let total_reward = Ratio::from(reward_amount); + let factor = { Ratio::new(U512::from(100), U512::from(100)) - rewards_ratio }; + let total_reward = Ratio::from(reward_amount) * factor; let rewarded_era = era_id .checked_sub(eras_back) .ok_or(Error::MissingSeigniorageRecipients)?; @@ -1666,7 +1678,7 @@ pub(crate) fn rewards_per_validator( // and increase their unbond request by the corresponding amount. results.push(RewardsPerValidator { - validator_reward: reward_amount, + validator_reward: total_reward.to_integer(), delegator_rewards: BTreeMap::new(), }); continue; @@ -1711,7 +1723,7 @@ pub(crate) fn rewards_per_validator( let total_delegator_payout: U512 = delegator_rewards.iter().map(|(_, &amount)| amount).sum(); - let validator_reward = reward_amount - total_delegator_payout; + let validator_reward = { total_reward - Ratio::from(total_delegator_payout) }.to_integer(); results.push(RewardsPerValidator { validator_reward, diff --git a/storage/src/system/genesis/account_contract_installer.rs b/storage/src/system/genesis/account_contract_installer.rs index 10fcbccdb6..add26fb7a8 100644 --- a/storage/src/system/genesis/account_contract_installer.rs +++ b/storage/src/system/genesis/account_contract_installer.rs @@ -14,6 +14,7 @@ use crate::{ genesis::{GenesisError, DEFAULT_ADDRESS, NO_WASM}, protocol_upgrade::ProtocolUpgradeError, }, + tracking_copy::AddResult, AddressGenerator, TrackingCopy, }; use casper_types::{ @@ -43,7 +44,7 @@ use casper_types::{ mint, mint::{ ARG_ROUND_SEIGNIORAGE_RATE, MINT_GAS_HOLD_HANDLING_KEY, MINT_GAS_HOLD_INTERVAL_KEY, - ROUND_SEIGNIORAGE_RATE_KEY, TOTAL_SUPPLY_KEY, + MINT_SUSTAIN_PURSE_KEY, ROUND_SEIGNIORAGE_RATE_KEY, TOTAL_SUPPLY_KEY, }, standard_payment, SystemEntityType, AUCTION, HANDLE_PAYMENT, MINT, STANDARD_PAYMENT, }, @@ -52,7 +53,7 @@ use casper_types::{ ChainspecRegistry, Contract, ContractWasm, ContractWasmHash, Digest, EntityAddr, EntityKind, EntityVersions, EntryPointAddr, EntryPointValue, EntryPoints, EraId, GenesisAccount, GenesisConfig, Groups, HashAddr, Key, Motes, Package, PackageHash, PackageStatus, Phase, - ProtocolVersion, PublicKey, StoredValue, SystemHashRegistry, URef, U512, + ProtocolVersion, PublicKey, RewardsHandling, StoredValue, SystemHashRegistry, URef, U512, }; pub struct AccountContractInstaller @@ -95,7 +96,7 @@ where self.tracking_copy.borrow().effects() } - fn create_mint(&mut self) -> Result> { + fn create_mint(&mut self) -> Result<(Key, Key), Box> { let round_seigniorage_rate_uref = { let round_seigniorage_rate_uref = self @@ -210,7 +211,7 @@ where .write(Key::SystemEntityRegistry, StoredValue::CLValue(cl_registry)); } - Ok(total_supply_uref.into()) + Ok((total_supply_uref.into(), Key::Hash(mint_hash.value()))) } fn create_handle_payment( @@ -538,7 +539,7 @@ where &self, total_supply_key: Key, payment_purse_uref: URef, - ) -> Result<(), Box> { + ) -> Result, Box> { let accounts = { let mut ret: Vec = self.config.accounts_iter().cloned().collect(); let system_account = GenesisAccount::system(); @@ -560,6 +561,7 @@ where } let mut total_supply = U512::zero(); + let mut sustain_purse = None; for account in accounts { let account_hash = account.account_hash(); @@ -572,6 +574,10 @@ where _ => self.create_purse(account.balance().value())?, }; + if self.config.rewards_ratio().is_some() && account.is_sustain_account() { + sustain_purse = Some(main_purse) + } + let key = Key::Account(account_hash); let stored_value = StoredValue::Account(Account::create( account_hash, @@ -592,7 +598,7 @@ where ), ); - Ok(()) + Ok(sustain_purse) } fn initial_seigniorage_recipients( @@ -750,6 +756,37 @@ where Ok(()) } + pub(crate) fn handle_sustain_purse( + &mut self, + sustain_purse: Option, + mint_key: Key, + ) -> Result<(), Box> { + if sustain_purse.is_none() { + return Ok(()); + } + + // This is safe because we early exit on the none case + let sustain_purse = sustain_purse.unwrap(); + let named_key_value = StoredValue::CLValue( + CLValue::from_t((MINT_SUSTAIN_PURSE_KEY.to_string(), Key::URef(sustain_purse))) + .map_err(|cl_error| Box::new(GenesisError::CLValue(cl_error.to_string())))?, + ); + + match self + .tracking_copy + .borrow_mut() + .add(mint_key, named_key_value) + { + Err(storage_error) => Err(Box::new(GenesisError::TrackingCopy(storage_error))), + Ok(AddResult::Success) => Ok(()), + Ok(AddResult::KeyNotFound(_)) => Err(Box::new(GenesisError::InvalidMintKey)), + Ok(AddResult::TypeMismatch(_)) | Ok(AddResult::Transform(_)) => Err(Box::new( + GenesisError::CLValue("Unable to add sustain purse".to_string()), + )), + Ok(AddResult::Serialization(error)) => Err(Box::new(GenesisError::Bytesrepr(error))), + } + } + /// Performs a complete system installation. pub(crate) fn install( &mut self, @@ -757,12 +794,14 @@ where ) -> Result<(), Box> { // self.setup_system_account()?; // Create mint - let total_supply_key = self.create_mint()?; + let (total_supply_key, mint_key) = self.create_mint()?; let payment_purse_uref = self.create_purse(U512::zero())?; // Create all genesis accounts - self.create_accounts(total_supply_key, payment_purse_uref)?; + let sustain_purse = self.create_accounts(total_supply_key, payment_purse_uref)?; + + self.handle_sustain_purse(sustain_purse, mint_key)?; // Create the auction and setup the stake of all genesis validators. self.create_auction(total_supply_key)?; diff --git a/storage/src/system/protocol_upgrade.rs b/storage/src/system/protocol_upgrade.rs index 7b81f22b05..2e283e044c 100644 --- a/storage/src/system/protocol_upgrade.rs +++ b/storage/src/system/protocol_upgrade.rs @@ -27,8 +27,8 @@ use casper_types::{ }, handle_payment::{ACCUMULATION_PURSE_KEY, PAYMENT_PURSE_KEY}, mint::{ - MINT_GAS_HOLD_HANDLING_KEY, MINT_GAS_HOLD_INTERVAL_KEY, ROUND_SEIGNIORAGE_RATE_KEY, - TOTAL_SUPPLY_KEY, + MINT_GAS_HOLD_HANDLING_KEY, MINT_GAS_HOLD_INTERVAL_KEY, MINT_SUSTAIN_PURSE_KEY, + ROUND_SEIGNIORAGE_RATE_KEY, TOTAL_SUPPLY_KEY, }, SystemEntityType, AUCTION, HANDLE_PAYMENT, MINT, }, @@ -36,12 +36,13 @@ use casper_types::{ ByteCodeKind, CLValue, CLValueError, Contract, Digest, EntityAddr, EntityVersionKey, EntityVersions, EntryPointAddr, EntryPointValue, EntryPoints, EraId, FeeHandling, Groups, HashAddr, Key, KeyTag, Motes, Package, PackageHash, PackageStatus, Phase, - ProtocolUpgradeConfig, ProtocolVersion, PublicKey, StoredValue, SystemHashRegistry, URef, U512, + ProtocolUpgradeConfig, ProtocolVersion, PublicKey, RewardsHandling, StoredValue, + SystemHashRegistry, URef, REWARDS_HANDLING_RATIO_TAG, U512, }; use crate::{ global_state::state::StateProvider, - tracking_copy::{TrackingCopy, TrackingCopyEntityExt, TrackingCopyExt}, + tracking_copy::{AddResult, TrackingCopy, TrackingCopyEntityExt, TrackingCopyExt}, AddressGenerator, }; @@ -217,6 +218,7 @@ where )?; self.handle_era_info_migration()?; self.handle_seignorage_snapshot_migration(system_entity_addresses.auction())?; + self.handle_rewards_handling(system_entity_addresses.mint())?; Ok(self.tracking_copy) } @@ -1568,6 +1570,70 @@ where Ok(()) } + /// Write or prune away the rewards handling entry in GS. + pub fn handle_rewards_handling(&mut self, mint: HashAddr) -> Result<(), ProtocolUpgradeError> { + let rewards_handling = self.config.rewards_handling(); + let rewards_handling_key = self + .tracking_copy + .read(&Key::RewardsHandling) + .map_err(ProtocolUpgradeError::TrackingCopy)?; + + match rewards_handling { + RewardsHandling::Standard => { + if let Some(StoredValue::CLValue(_)) = rewards_handling_key { + self.tracking_copy.prune(Key::RewardsHandling); + } + } + RewardsHandling::Sustain { + ratio, + purse_address, + } => { + let sustain_purse = URef::from_formatted_str(&purse_address).map_err(|_| { + ProtocolUpgradeError::CLValue("unable to create sustain purse".to_string()) + })?; + + let value = StoredValue::CLValue( + CLValue::from_t((MINT_SUSTAIN_PURSE_KEY.to_string(), Key::URef(sustain_purse))) + .map_err(|_| { + ProtocolUpgradeError::Bytesrepr("new_auction_delay".to_string()) + })?, + ); + + let mint_key = if self.config.enable_addressable_entity() { + Key::AddressableEntity(EntityAddr::System(mint)) + } else { + Key::Hash(mint) + }; + match self.tracking_copy.add(mint_key, value) { + Ok(AddResult::Success) => { + info!("Successfully added sustain purse to mint named keys") + } + Ok(_) | Err(_) => { + return Err(ProtocolUpgradeError::CLValue( + "Unable to add sustain purse".to_string(), + )) + } + }; + + let rewards_ratio = ratio + .to_bytes() + .map_err(|err| ProtocolUpgradeError::Bytesrepr(err.to_string()))?; + let rewards_handling_map = { + let mut ret = BTreeMap::new(); + ret.insert(REWARDS_HANDLING_RATIO_TAG, rewards_ratio); + CLValue::from_t(ret) + .map_err(|cl| ProtocolUpgradeError::CLValue(cl.to_string()))? + }; + self.tracking_copy.write( + Key::RewardsHandling, + StoredValue::CLValue(rewards_handling_map), + ); + } + } + + Ok(()) + } + /// Handle global state updates. pub fn handle_global_state_updates(&mut self) { debug!("handle global state updates"); diff --git a/storage/src/system/runtime_native.rs b/storage/src/system/runtime_native.rs index 4628c79b1c..803418cb62 100644 --- a/storage/src/system/runtime_native.rs +++ b/storage/src/system/runtime_native.rs @@ -5,8 +5,8 @@ use crate::{ }; use casper_types::{ account::AccountHash, contracts::NamedKeys, Chainspec, ContextAccessRights, EntityAddr, - FeeHandling, Key, Phase, ProtocolVersion, PublicKey, RefundHandling, RuntimeFootprint, - StoredValue, TransactionHash, Transfer, URef, U512, + FeeHandling, Key, Phase, ProtocolVersion, PublicKey, RefundHandling, RewardsHandling, + RuntimeFootprint, StoredValue, TransactionHash, Transfer, URef, U512, }; use num_rational::Ratio; use parking_lot::RwLock; @@ -31,6 +31,7 @@ pub struct Config { credit_cap: Ratio, enable_addressable_entity: bool, native_transfer_cost: u32, + rewards_handling: RewardsHandling, } impl Config { @@ -52,6 +53,7 @@ impl Config { credit_cap: Ratio, enable_addressable_entity: bool, native_transfer_cost: u32, + rewards_handling: RewardsHandling, ) -> Self { Config { transfer_config, @@ -69,6 +71,7 @@ impl Config { credit_cap, enable_addressable_entity, native_transfer_cost, + rewards_handling, } } @@ -92,6 +95,7 @@ impl Config { ); let enable_addressable_entity = chainspec.core_config.enable_addressable_entity; let native_transfer_cost = chainspec.system_costs_config.mint_costs().transfer; + let rewards_handling = chainspec.core_config.rewards_handling.clone(); Config::new( transfer_config, fee_handling, @@ -108,6 +112,7 @@ impl Config { credit_cap, enable_addressable_entity, native_transfer_cost, + rewards_handling, ) } @@ -181,6 +186,11 @@ impl Config { self.enable_addressable_entity } + /// Rewards handling for the runtime native config. + pub fn rewards_handling(&self) -> RewardsHandling { + self.rewards_handling.clone() + } + /// Changes the transfer config. pub fn set_transfer_config(self, transfer_config: TransferConfig) -> Self { Config { @@ -199,6 +209,7 @@ impl Config { credit_cap: self.credit_cap, enable_addressable_entity: self.enable_addressable_entity, native_transfer_cost: self.native_transfer_cost, + rewards_handling: self.rewards_handling, } } } diff --git a/types/src/chainspec.rs b/types/src/chainspec.rs index d6f84d52e8..4b25f79c11 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -15,6 +15,7 @@ mod next_upgrade; mod pricing_handling; mod protocol_config; mod refund_handling; +mod rewards_handling; mod transaction_config; mod upgrade_config; mod vacancy_config; @@ -61,6 +62,7 @@ pub use next_upgrade::NextUpgrade; pub use pricing_handling::PricingHandling; pub use protocol_config::ProtocolConfig; pub use refund_handling::RefundHandling; +pub use rewards_handling::{RewardsHandling, REWARDS_HANDLING_RATIO_TAG}; pub use transaction_config::{ DeployConfig, TransactionConfig, TransactionLaneDefinition, TransactionV1Config, }; @@ -194,6 +196,7 @@ impl Chainspec { let maximum_delegation_amount = self.core_config.maximum_delegation_amount; let minimum_delegation_amount = self.core_config.minimum_delegation_amount; let enable_addressable_entity = self.core_config.enable_addressable_entity; + let rewards_handling = self.core_config.rewards_handling.clone(); Ok(ProtocolUpgradeConfig::new( pre_state_hash, @@ -214,6 +217,7 @@ impl Chainspec { maximum_delegation_amount, minimum_delegation_amount, enable_addressable_entity, + rewards_handling, )) } diff --git a/types/src/chainspec/accounts_config/genesis.rs b/types/src/chainspec/accounts_config/genesis.rs index 4acd1d62d7..86f789e956 100644 --- a/types/src/chainspec/accounts_config/genesis.rs +++ b/types/src/chainspec/accounts_config/genesis.rs @@ -25,6 +25,7 @@ enum GenesisAccountTag { Account = 1, Delegator = 2, Administrator = 3, + SustainAccount = 4, } /// Represents details about genesis account's validator status. @@ -182,6 +183,8 @@ pub enum GenesisAccount { /// /// This variant makes sense for some private chains. Administrator(AdministratorAccount), + /// An account to associate for the sustain purse + SustainAccount { public_key: PublicKey }, } impl From for GenesisAccount { @@ -224,6 +227,11 @@ impl GenesisAccount { } } + /// Create a new sustain account. + pub fn sustain(public_key: PublicKey) -> Self { + Self::SustainAccount { public_key } + } + /// The public key (if any) associated with the account. pub fn public_key(&self) -> PublicKey { match self { @@ -236,6 +244,7 @@ impl GenesisAccount { GenesisAccount::Administrator(AdministratorAccount { public_key, .. }) => { public_key.clone() } + GenesisAccount::SustainAccount { public_key } => public_key.clone(), } } @@ -251,6 +260,7 @@ impl GenesisAccount { GenesisAccount::Administrator(AdministratorAccount { public_key, .. }) => { public_key.to_account_hash() } + GenesisAccount::SustainAccount { public_key } => public_key.to_account_hash(), } } @@ -261,6 +271,7 @@ impl GenesisAccount { GenesisAccount::Account { balance, .. } => *balance, GenesisAccount::Delegator { balance, .. } => *balance, GenesisAccount::Administrator(AdministratorAccount { balance, .. }) => *balance, + GenesisAccount::SustainAccount { .. } => Motes::zero(), } } @@ -289,6 +300,7 @@ impl GenesisAccount { // validator set is created at the genesis. Motes::zero() } + GenesisAccount::SustainAccount { .. } => Motes::zero(), } } @@ -309,6 +321,7 @@ impl GenesisAccount { DelegationRate::MAX } GenesisAccount::Administrator(AdministratorAccount { .. }) => DelegationRate::MAX, + GenesisAccount::SustainAccount { .. } => DelegationRate::MAX, } } @@ -328,7 +341,8 @@ impl GenesisAccount { validator: None, .. } | GenesisAccount::Delegator { .. } - | GenesisAccount::Administrator(AdministratorAccount { .. }) => false, + | GenesisAccount::Administrator(AdministratorAccount { .. }) + | GenesisAccount::SustainAccount { .. } => false, } } @@ -384,9 +398,14 @@ impl GenesisAccount { } GenesisAccount::System | GenesisAccount::Delegator { .. } - | GenesisAccount::Administrator(_) => false, + | GenesisAccount::Administrator(_) + | GenesisAccount::SustainAccount { .. } => false, } } + + pub fn is_sustain_account(&self) -> bool { + matches!(self, Self::SustainAccount { .. }) + } } #[cfg(any(feature = "testing", test))] @@ -436,6 +455,10 @@ impl ToBytes for GenesisAccount { buffer.push(GenesisAccountTag::Administrator as u8); buffer.extend(administrator_account.to_bytes()?); } + GenesisAccount::SustainAccount { public_key } => { + buffer.push(GenesisAccountTag::SustainAccount as u8); + buffer.extend(public_key.to_bytes()?); + } } Ok(buffer) } @@ -468,6 +491,9 @@ impl ToBytes for GenesisAccount { GenesisAccount::Administrator(administrator_account) => { administrator_account.serialized_length() + TAG_LENGTH } + GenesisAccount::SustainAccount { public_key } => { + public_key.serialized_length() + TAG_LENGTH + } } } } @@ -506,6 +532,10 @@ impl FromBytes for GenesisAccount { let genesis_account = GenesisAccount::Administrator(administrator_account); Ok((genesis_account, remainder)) } + tag if tag == GenesisAccountTag::SustainAccount as u8 => { + let (public_key, remainder) = FromBytes::from_bytes(remainder)?; + Ok((GenesisAccount::SustainAccount { public_key }, remainder)) + } _ => Err(bytesrepr::Error::Formatting), } } diff --git a/types/src/chainspec/core_config.rs b/types/src/chainspec/core_config.rs index 3d01a50348..83592196d3 100644 --- a/types/src/chainspec/core_config.rs +++ b/types/src/chainspec/core_config.rs @@ -17,6 +17,7 @@ use serde::{ use crate::testing::TestRng; use crate::{ bytesrepr::{self, FromBytes, ToBytes}, + chainspec::rewards_handling::RewardsHandling, ProtocolVersion, PublicKey, TimeDiff, U512, }; @@ -190,6 +191,8 @@ pub struct CoreConfig { /// The flag on whether the engine will return an error for multiple /// entity versions. pub trap_on_ambiguous_entity_version: bool, + #[cfg_attr(feature = "datasize", data_size(skip))] + pub rewards_handling: RewardsHandling, } impl CoreConfig { @@ -335,6 +338,7 @@ impl CoreConfig { enable_addressable_entity: DEFAULT_ENABLE_ENTITY, baseline_motes_amount: DEFAULT_BASELINE_MOTES_AMOUNT, trap_on_ambiguous_entity_version: false, + rewards_handling: RewardsHandling::Standard, } } } @@ -382,6 +386,7 @@ impl Default for CoreConfig { enable_addressable_entity: DEFAULT_ENABLE_ENTITY, baseline_motes_amount: DEFAULT_BASELINE_MOTES_AMOUNT, trap_on_ambiguous_entity_version: false, + rewards_handling: RewardsHandling::Standard, } } } @@ -431,6 +436,7 @@ impl ToBytes for CoreConfig { buffer.extend(self.enable_addressable_entity.to_bytes()?); buffer.extend(self.baseline_motes_amount.to_bytes()?); buffer.extend(self.trap_on_ambiguous_entity_version.to_bytes()?); + buffer.extend(self.rewards_handling.to_bytes()?); Ok(buffer) } @@ -476,6 +482,7 @@ impl ToBytes for CoreConfig { + self.enable_addressable_entity.serialized_length() + self.baseline_motes_amount.serialized_length() + self.trap_on_ambiguous_entity_version.serialized_length() + + self.rewards_handling.serialized_length() } } @@ -521,6 +528,7 @@ impl FromBytes for CoreConfig { let (enable_addressable_entity, remainder) = FromBytes::from_bytes(remainder)?; let (baseline_motes_amount, remainder) = u64::from_bytes(remainder)?; let (trap_on_ambiguous_entity_version, remainder) = bool::from_bytes(remainder)?; + let (rewards_handling, remainder) = RewardsHandling::from_bytes(remainder)?; let config = CoreConfig { era_duration, minimum_era_height, @@ -561,6 +569,7 @@ impl FromBytes for CoreConfig { enable_addressable_entity, baseline_motes_amount, trap_on_ambiguous_entity_version, + rewards_handling, }; Ok((config, remainder)) } diff --git a/types/src/chainspec/genesis_config.rs b/types/src/chainspec/genesis_config.rs index ce05950792..8eb98ed57c 100644 --- a/types/src/chainspec/genesis_config.rs +++ b/types/src/chainspec/genesis_config.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use crate::{ AdministratorAccount, Chainspec, GenesisAccount, GenesisValidator, HoldBalanceHandling, Motes, - PublicKey, SystemConfig, WasmConfig, + PublicKey, RewardsHandling, SystemConfig, WasmConfig, }; use super::StorageCosts; @@ -33,6 +33,7 @@ pub struct GenesisConfig { gas_hold_balance_handling: HoldBalanceHandling, gas_hold_interval_millis: u64, enable_addressable_entity: bool, + rewards_ratio: Option>, storage_costs: StorageCosts, } @@ -52,6 +53,7 @@ impl GenesisConfig { gas_hold_balance_handling: HoldBalanceHandling, gas_hold_interval_millis: u64, enable_addressable_entity: bool, + rewards_handling: Option>, storage_costs: StorageCosts, ) -> GenesisConfig { GenesisConfig { @@ -67,6 +69,7 @@ impl GenesisConfig { gas_hold_balance_handling, gas_hold_interval_millis, enable_addressable_entity, + rewards_ratio: rewards_handling, storage_costs, } } @@ -182,6 +185,13 @@ impl GenesisConfig { genesis_account.try_set_validator(genesis_validator); } } + + pub fn rewards_ratio(&self) -> Option> { + self.rewards_ratio + } + pub fn push_rewards_ratio(&mut self, rewards_ratio: Ratio) { + self.rewards_ratio = Some(rewards_ratio); + } } #[cfg(any(feature = "testing", test))] @@ -226,6 +236,7 @@ impl Distribution for Standard { gas_hold_balance_handling, gas_hold_interval_millis, enable_addressable_entity: false, + rewards_ratio: None, storage_costs, } } @@ -240,6 +251,10 @@ impl From<&Chainspec> for GenesisConfig { .map_or(0, |timestamp| timestamp.millis()); let gas_hold_interval_millis = chainspec.core_config.gas_hold_interval.millis(); let gas_hold_balance_handling = chainspec.core_config.gas_hold_balance_handling; + let rewards_ratio = match chainspec.core_config.rewards_handling { + RewardsHandling::Standard => None, + RewardsHandling::Sustain { ratio, .. } => Some(ratio), + }; let storage_costs = chainspec.storage_costs; GenesisConfig { accounts: chainspec.network_config.accounts_config.clone().into(), @@ -254,6 +269,7 @@ impl From<&Chainspec> for GenesisConfig { gas_hold_balance_handling, gas_hold_interval_millis, enable_addressable_entity: chainspec.core_config.enable_addressable_entity, + rewards_ratio, storage_costs, } } diff --git a/types/src/chainspec/rewards_handling.rs b/types/src/chainspec/rewards_handling.rs new file mode 100644 index 0000000000..3c59cb4109 --- /dev/null +++ b/types/src/chainspec/rewards_handling.rs @@ -0,0 +1,138 @@ +/// Configuration options of reward handling that are executed as part of rewards distribution. +use num_rational::Ratio; +use serde::{Deserialize, Serialize}; + +use crate::{ + bytesrepr::{self, Error, FromBytes, ToBytes}, + uref::FromStrError, + URef, +}; + +pub const REWARDS_HANDLING_RATIO_TAG: u8 = 0; + +const REWARDS_HANDLING_STANDARD_TAG: u8 = 0; + +const REWARDS_HANDLING_SUSTAIN_TAG: u8 = 1; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum RewardsHandling { + #[default] + Standard, + + Sustain { + ratio: Ratio, + purse_address: String, + }, +} + +impl RewardsHandling { + pub fn purse_address(&self) -> Option> { + match self { + Self::Standard => None, + Self::Sustain { purse_address, .. } => Some(URef::from_formatted_str(purse_address)), + } + } + + pub fn maybe_ratio(&self) -> Option> { + match self { + Self::Standard => None, + Self::Sustain { ratio, .. } => Some(*ratio), + } + } + + pub fn is_valid_configuration(&self) -> bool { + match self { + Self::Standard => true, + Self::Sustain { + ratio, + purse_address, + } => { + if *ratio.numer() > *ratio.denom() { + return false; + } + + if URef::from_formatted_str(purse_address).is_err() { + return false; + } + + true + } + } + } +} + +impl ToBytes for RewardsHandling { + fn to_bytes(&self) -> Result, Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + + match self { + Self::Standard => { + buffer.push(REWARDS_HANDLING_STANDARD_TAG); + } + Self::Sustain { + ratio, + purse_address, + } => { + buffer.push(REWARDS_HANDLING_SUSTAIN_TAG); + buffer.extend(ratio.to_bytes()?); + buffer.extend(purse_address.to_bytes()?); + } + } + + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + 1 + match self { + RewardsHandling::Standard => 0, + RewardsHandling::Sustain { + ratio, + purse_address, + } => ratio.serialized_length() + purse_address.serialized_length(), + } + } +} + +impl FromBytes for RewardsHandling { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error> { + let (tag, rem) = u8::from_bytes(bytes)?; + + match tag { + REWARDS_HANDLING_STANDARD_TAG => Ok((RewardsHandling::Standard, rem)), + REWARDS_HANDLING_SUSTAIN_TAG => { + let (ratio, rem) = FromBytes::from_bytes(rem)?; + let (purse_address, rem) = String::from_bytes(rem)?; + Ok(( + RewardsHandling::Sustain { + ratio, + purse_address, + }, + rem, + )) + } + _ => Err(Error::Formatting), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::URef; + + #[test] + fn bytesrepr_roundtrip_for_sustain() { + let rewards_handling = RewardsHandling::Sustain { + ratio: Ratio::new(49, 313), + purse_address: URef::default().to_formatted_string(), + }; + bytesrepr::test_serialization_roundtrip(&rewards_handling); + } + + #[test] + fn bytesrepr_roundtrip_for_standard() { + let rewards_handling = RewardsHandling::Standard; + bytesrepr::test_serialization_roundtrip(&rewards_handling); + } +} diff --git a/types/src/chainspec/upgrade_config.rs b/types/src/chainspec/upgrade_config.rs index 5ce68f9ce4..1e441c073b 100644 --- a/types/src/chainspec/upgrade_config.rs +++ b/types/src/chainspec/upgrade_config.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use crate::{ ChainspecRegistry, Digest, EraId, FeeHandling, HoldBalanceHandling, Key, ProtocolVersion, - StoredValue, + RewardsHandling, StoredValue, }; /// Represents the configuration of a protocol upgrade. @@ -28,6 +28,7 @@ pub struct ProtocolUpgradeConfig { maximum_delegation_amount: u64, minimum_delegation_amount: u64, enable_addressable_entity: bool, + rewards_handling: RewardsHandling, } impl ProtocolUpgradeConfig { @@ -52,6 +53,7 @@ impl ProtocolUpgradeConfig { maximum_delegation_amount: u64, minimum_delegation_amount: u64, enable_addressable_entity: bool, + rewards_handling: RewardsHandling, ) -> Self { ProtocolUpgradeConfig { pre_state_hash, @@ -72,6 +74,7 @@ impl ProtocolUpgradeConfig { maximum_delegation_amount, minimum_delegation_amount, enable_addressable_entity, + rewards_handling, } } @@ -168,4 +171,8 @@ impl ProtocolUpgradeConfig { pub fn enable_addressable_entity(&self) -> bool { self.enable_addressable_entity } + + pub fn rewards_handling(&self) -> RewardsHandling { + self.rewards_handling.clone() + } } diff --git a/types/src/key.rs b/types/src/key.rs index 90ee1f9a0f..141a278838 100644 --- a/types/src/key.rs +++ b/types/src/key.rs @@ -79,6 +79,7 @@ const BLOCK_GLOBAL_MESSAGE_COUNT_PREFIX: &str = "block-message-count-"; const BLOCK_GLOBAL_PROTOCOL_VERSION_PREFIX: &str = "block-protocol-version-"; const BLOCK_GLOBAL_ADDRESSABLE_ENTITY_PREFIX: &str = "block-addressable-entity-"; const STATE_PREFIX: &str = "state-"; +const REWARDS_HANDLING_PREFIX: &str = "rewards-handling-"; /// The number of bytes in a Blake2b hash pub const BLAKE2B_DIGEST_LENGTH: usize = 32; @@ -115,6 +116,8 @@ const KEY_CHAINSPEC_REGISTRY_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + PADDING_BYTES.len(); const KEY_CHECKSUM_REGISTRY_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + PADDING_BYTES.len(); +const KEY_REWARDS_HANDLING_SERIALIZED_LENGTH: usize = + KEY_ID_SERIALIZED_LENGTH + PADDING_BYTES.len(); const KEY_PACKAGE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + 32; const KEY_MESSAGE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + U8_SERIALIZED_LENGTH @@ -163,6 +166,7 @@ pub enum KeyTag { BalanceHold = 22, EntryPoint = 23, State = 24, + RewardsHandling = 25, } impl KeyTag { @@ -228,6 +232,7 @@ impl Display for KeyTag { KeyTag::BalanceHold => write!(f, "BalanceHold"), KeyTag::State => write!(f, "State"), KeyTag::EntryPoint => write!(f, "EntryPoint"), + KeyTag::RewardsHandling => write!(f, "RewardsHandling"), } } } @@ -278,6 +283,7 @@ impl FromBytes for KeyTag { tag if tag == KeyTag::BalanceHold as u8 => KeyTag::BalanceHold, tag if tag == KeyTag::EntryPoint as u8 => KeyTag::EntryPoint, tag if tag == KeyTag::State as u8 => KeyTag::State, + tag if tag == KeyTag::RewardsHandling as u8 => KeyTag::RewardsHandling, _ => return Err(Error::Formatting), }; Ok((tag, rem)) @@ -342,6 +348,8 @@ pub enum Key { EntryPoint(EntryPointAddr), /// A `Key` under which a contract's state lives. State(EntityAddr), + /// A `Key` under which we store rewards handling information + RewardsHandling, } #[cfg(feature = "json-schema")] @@ -416,6 +424,7 @@ pub enum FromStrError { EntryPoint(String), /// State key parse error. State(String), + RewardsHandling(String), /// Unknown prefix. UnknownPrefix, } @@ -501,6 +510,10 @@ impl Display for FromStrError { } FromStrError::UnknownPrefix => write!(f, "unknown prefix for key"), FromStrError::State(error) => write!(f, "state-key from string error: {}", error), + + FromStrError::RewardsHandling(error) => { + write!(f, "rewards-handling-key from string error: {}", error) + } } } } @@ -535,6 +548,7 @@ impl Key { Key::BalanceHold(_) => String::from("Key::BalanceHold"), Key::EntryPoint(_) => String::from("Key::EntryPoint"), Key::State(_) => String::from("Key::State"), + Key::RewardsHandling => String::from("Key::RewardsHandling"), } } @@ -663,6 +677,13 @@ impl Key { Key::EntryPoint(entry_point_addr) => { format!("{}", entry_point_addr) } + Key::RewardsHandling => { + format!( + "{}{}", + REWARDS_HANDLING_PREFIX, + base16::encode_lower(&PADDING_BYTES) + ) + } } } @@ -981,6 +1002,15 @@ impl Key { } } + if let Some(rewards_handling_padding) = input.strip_prefix(REWARDS_HANDLING_PREFIX) { + let padded_bytes = checksummed_hex::decode(rewards_handling_padding) + .map_err(|error| FromStrError::RewardsHandling(error.to_string()))?; + let _padding: [u8; 32] = TryFrom::try_from(padded_bytes.as_ref()).map_err(|_| { + FromStrError::RewardsHandling("Failed to deserialize era summary key".to_string()) + })?; + return Ok(Key::RewardsHandling); + } + Err(FromStrError::UnknownPrefix) } @@ -1452,6 +1482,11 @@ impl Display for Key { Key::State(entity_addr) => { write!(f, "Key::State({})", entity_addr) } + Key::RewardsHandling => write!( + f, + "Key::RewardsHandling({})", + base16::encode_lower(&PADDING_BYTES), + ), } } } @@ -1490,6 +1525,7 @@ impl Tagged for Key { Key::BalanceHold(_) => KeyTag::BalanceHold, Key::EntryPoint(_) => KeyTag::EntryPoint, Key::State(_) => KeyTag::State, + Key::RewardsHandling => KeyTag::RewardsHandling, } } } @@ -1607,6 +1643,7 @@ impl ToBytes for Key { U8_SERIALIZED_LENGTH + entry_point_addr.serialized_length() } Key::State(entity_addr) => KEY_ID_SERIALIZED_LENGTH + entity_addr.serialized_length(), + Key::RewardsHandling => KEY_REWARDS_HANDLING_SERIALIZED_LENGTH, } } @@ -1627,7 +1664,8 @@ impl ToBytes for Key { Key::SystemEntityRegistry | Key::EraSummary | Key::ChainspecRegistry - | Key::ChecksumRegistry => PADDING_BYTES.write_bytes(writer), + | Key::ChecksumRegistry + | Key::RewardsHandling => PADDING_BYTES.write_bytes(writer), Key::BlockGlobal(addr) => { addr.write_bytes(writer)?; BLOCK_GLOBAL_PADDING_BYTES.write_bytes(writer) @@ -1759,6 +1797,10 @@ impl FromBytes for Key { let (entity_addr, rem) = EntityAddr::from_bytes(remainder)?; Ok((Key::State(entity_addr), rem)) } + KeyTag::RewardsHandling => { + let (_, rem) = <[u8; 32]>::from_bytes(remainder)?; + Ok((Key::RewardsHandling, rem)) + } } } } @@ -1793,6 +1835,7 @@ fn please_add_to_distribution_impl(key: Key) { Key::BalanceHold(_) => unimplemented!(), Key::EntryPoint(_) => unimplemented!(), Key::State(_) => unimplemented!(), + Key::RewardsHandling => unimplemented!(), } } @@ -1861,6 +1904,7 @@ mod serde_helpers { BalanceHold(&'a BalanceHoldAddr), EntryPoint(&'a EntryPointAddr), State(&'a EntityAddr), + RewardsHandling, } #[derive(Deserialize)] @@ -1891,6 +1935,7 @@ mod serde_helpers { BalanceHold(BalanceHoldAddr), EntryPoint(EntryPointAddr), State(EntityAddr), + RewardsHandling, } impl<'a> From<&'a Key> for BinarySerHelper<'a> { @@ -1925,6 +1970,7 @@ mod serde_helpers { } Key::EntryPoint(entry_point_addr) => BinarySerHelper::EntryPoint(entry_point_addr), Key::State(entity_addr) => BinarySerHelper::State(entity_addr), + Key::RewardsHandling => BinarySerHelper::RewardsHandling, } } } @@ -1963,6 +2009,7 @@ mod serde_helpers { Key::EntryPoint(entry_point_addr) } BinaryDeserHelper::State(entity_addr) => Key::State(entity_addr), + BinaryDeserHelper::RewardsHandling => Key::RewardsHandling, } } } diff --git a/types/src/lib.rs b/types/src/lib.rs index ab8e405023..0b29fed24b 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -130,11 +130,11 @@ pub use chainspec::{ GlobalStateUpdateError, HandlePaymentCosts, HighwayConfig, HoldBalanceHandling, HostFunction, HostFunctionCost, HostFunctionCostsV1, HostFunctionCostsV2, HostFunctionV2, LegacyRequiredFinality, MessageLimits, MintCosts, NetworkConfig, NextUpgrade, OpcodeCosts, - PricingHandling, ProtocolConfig, ProtocolUpgradeConfig, RefundHandling, StandardPaymentCosts, - StorageCosts, SystemConfig, TransactionConfig, TransactionLaneDefinition, TransactionV1Config, - VacancyConfig, ValidatorConfig, WasmConfig, WasmV1Config, WasmV2Config, + PricingHandling, ProtocolConfig, ProtocolUpgradeConfig, RefundHandling, RewardsHandling, + StandardPaymentCosts, StorageCosts, SystemConfig, TransactionConfig, TransactionLaneDefinition, + TransactionV1Config, VacancyConfig, ValidatorConfig, WasmConfig, WasmV1Config, WasmV2Config, DEFAULT_BASELINE_MOTES_AMOUNT, DEFAULT_GAS_HOLD_INTERVAL, DEFAULT_HOST_FUNCTION_NEW_DICTIONARY, - DEFAULT_MINIMUM_BID_AMOUNT, DEFAULT_REFUND_HANDLING, + DEFAULT_MINIMUM_BID_AMOUNT, DEFAULT_REFUND_HANDLING, REWARDS_HANDLING_RATIO_TAG, }; #[cfg(any(all(feature = "std", feature = "testing"), test))] pub use chainspec::{ diff --git a/types/src/system/mint/constants.rs b/types/src/system/mint/constants.rs index a44aa1fcbf..114a7eba03 100644 --- a/types/src/system/mint/constants.rs +++ b/types/src/system/mint/constants.rs @@ -44,3 +44,5 @@ pub const ROUND_SEIGNIORAGE_RATE_KEY: &str = "round_seigniorage_rate"; pub const MINT_GAS_HOLD_HANDLING_KEY: &str = "gas_hold_handling"; /// Storage for gas hold interval. pub const MINT_GAS_HOLD_INTERVAL_KEY: &str = "gas_hold_interval"; +/// Named key for sustain purse +pub const MINT_SUSTAIN_PURSE_KEY: &str = "sustain_purse"; 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 c96b826371..d04b75f8b2 100644 --- a/utils/global-state-update-gen/src/generic/state_tracker.rs +++ b/utils/global-state-update-gen/src/generic/state_tracker.rs @@ -356,7 +356,6 @@ impl StateTracker { .expect("should have bonding purse") != bonding_purse { - println!("foo"); self.set_purse_balance(existing_bid.bonding_purse().unwrap(), U512::zero()); self.set_purse_balance(bonding_purse, previously_bonded); // the old bonding purse gets zeroed - the unbonds will get invalid, anyway