From f822f26742a1cc33b385376e41d89d99875bdd55 Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Thu, 19 Feb 2026 12:33:18 -0600 Subject: [PATCH 01/12] Initial wire up of rewards change --- resources/local/chainspec.toml.in | 1 + resources/production/chainspec.toml | 2 + storage/src/system/auction.rs | 15 +++ storage/src/system/auction/detail.rs | 2 +- types/src/chainspec.rs | 1 + types/src/chainspec/core_config.rs | 9 ++ types/src/chainspec/rewards_handling.rs | 118 ++++++++++++++++++++++++ 7 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 types/src/chainspec/rewards_handling.rs diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index 72d35074f1..8636667761 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 = 'sustain', ratio = [2, 100], purse_address = 'uref-addr' } [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..4367c1a5d7 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 +reward_handling = { type = 'sustain', ratio = [2, 100], purse_address = 'uref-addr' } + [highway] # Highway dynamically chooses its round length, between minimum_block_time and maximum_round_length. diff --git a/storage/src/system/auction.rs b/storage/src/system/auction.rs index 3eaaf35e91..5c7d01c485 100644 --- a/storage/src/system/auction.rs +++ b/storage/src/system/auction.rs @@ -639,6 +639,21 @@ pub trait Auction: 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 skim = Ratio::new(U512::from(50), U512::one()); + + let _share = (skim * total).to_integer(); + debug!("reading seigniorage recipients snapshot"); let seigniorage_recipients_snapshot = detail::get_seigniorage_recipients_snapshot(self)?; let current_era_id = detail::get_era_id(self)?; diff --git a/storage/src/system/auction/detail.rs b/storage/src/system/auction/detail.rs index bf099e1293..d528c359bf 100644 --- a/storage/src/system/auction/detail.rs +++ b/storage/src/system/auction/detail.rs @@ -1625,7 +1625,7 @@ 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 total_reward = Ratio::from(reward_amount) * Ratio::new(U512::from(98), U512::from(100)); let rewarded_era = era_id .checked_sub(eras_back) .ok_or(Error::MissingSeigniorageRecipients)?; diff --git a/types/src/chainspec.rs b/types/src/chainspec.rs index d6f84d52e8..ccf7f7a148 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; 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/rewards_handling.rs b/types/src/chainspec/rewards_handling.rs new file mode 100644 index 0000000000..76efbea665 --- /dev/null +++ b/types/src/chainspec/rewards_handling.rs @@ -0,0 +1,118 @@ +/// Configuration options of refund handling that are executed as part of handle payment +/// finalization. +use num_rational::Ratio; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; + +use crate::{ + bytesrepr::{self, Error, FromBytes, ToBytes}, + uref::FromStrError, + RefundHandling, URef, +}; + +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), + } + } +} + +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); + } +} From 03eb7e8b0f8917e20bdc19679c3ec5931368ae8e Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Thu, 19 Feb 2026 13:45:59 -0600 Subject: [PATCH 02/12] Add tests for new sustain mode --- .../system_contracts/auction/distribute.rs | 594 ++++++++++++++++++ storage/src/system/auction/detail.rs | 10 +- 2 files changed, 602 insertions(+), 2 deletions(-) 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..c0ae5cf65a 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 @@ -1673,6 +1673,246 @@ 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 = 800_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(); + + builder.run_genesis(LOCAL_GENESIS_REQUEST.clone()); + + 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 expected_total_reward = + Ratio::from(expected_total_reward) * Ratio::new(U512::from(98), U512::from(100)); + println!("foo {:?}", expected_total_reward); + + 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 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 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() + }; + + println!("share {:?}", delegators_share); + + 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; + println!( + "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() { @@ -2363,6 +2603,360 @@ fn should_distribute_with_multiple_validators_and_shared_delegator() { ); } +#[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(); + + builder.run_genesis(LOCAL_GENESIS_REQUEST.clone()); + + 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); + + 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 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 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_increase_total_supply_after_distribute() { diff --git a/storage/src/system/auction/detail.rs b/storage/src/system/auction/detail.rs index d528c359bf..c65d3a5d02 100644 --- a/storage/src/system/auction/detail.rs +++ b/storage/src/system/auction/detail.rs @@ -1625,7 +1625,9 @@ 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) { + println!("reward amount: {:?}", reward_amount); let total_reward = Ratio::from(reward_amount) * Ratio::new(U512::from(98), U512::from(100)); + println!("total reward: {:?}", total_reward); let rewarded_era = era_id .checked_sub(eras_back) .ok_or(Error::MissingSeigniorageRecipients)?; @@ -1666,7 +1668,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 +1713,11 @@ 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(); + println!( + "new rewards: {:?}, {total_delegator_payout}", + validator_reward + ); results.push(RewardsPerValidator { validator_reward, From 51abda6ab3801583d3239b1562a62d337130799a Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Mon, 23 Feb 2026 11:18:38 -0600 Subject: [PATCH 03/12] Wire up rewards handling in distribute --- .../src/engine_state/engine_config.rs | 16 ++++- execution_engine/src/runtime/mod.rs | 5 +- .../src/transfer_request_builder.rs | 3 +- .../test_support/src/wasm_test_builder.rs | 71 ++++++++++++++++++- .../system_contracts/auction/distribute.rs | 68 ++++++++++++++---- node/src/components/binary_port.rs | 21 +++++- resources/local/chainspec.toml.in | 2 +- storage/src/global_state/state/mod.rs | 4 +- storage/src/system/auction.rs | 29 ++++++-- storage/src/system/auction/detail.rs | 32 ++++++--- storage/src/system/runtime_native.rs | 15 +++- types/src/chainspec.rs | 1 + types/src/chainspec/rewards_handling.rs | 3 +- types/src/lib.rs | 6 +- 14 files changed, 230 insertions(+), 46 deletions(-) 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..c1cf22501e 100644 --- a/execution_engine/src/runtime/mod.rs +++ b/execution_engine/src/runtime/mod.rs @@ -1254,8 +1254,11 @@ 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)?; + runtime + .distribute(rewards, rewards_handling) + .map_err(Self::reverter)?; CLValue::from_t(()).map_err(Self::reverter) })(), 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/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/system_contracts/auction/distribute.rs b/execution_engine_testing/tests/src/test/system_contracts/auction/distribute.rs index c0ae5cf65a..ac8f7cb07c 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 @@ -14,7 +14,7 @@ use casper_engine_test_support::{ 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_storage::data_access_layer::{AuctionMethod, BlockRewardsRequest}; use casper_types::{ self, account::AccountHash, @@ -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, + AccessRights, EntityAddr, EraId, ProtocolVersion, PublicKey, RewardsHandling, SecretKey, + Timestamp, URef, DEFAULT_MINIMUM_BID_AMOUNT, U512, }; const ARG_ENTRY_POINT: &str = "entry_point"; @@ -1779,8 +1779,9 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { let expected_total_reward_integer = expected_total_reward.to_integer(); assert_eq!(total_payout, expected_total_reward_integer); - let expected_total_reward = - Ratio::from(expected_total_reward) * Ratio::new(U512::from(98), U512::from(100)); + let sustain_ratio_as_u512 = Ratio::new(U512::from(1), U512::from(2)); + + let expected_total_reward = expected_total_reward * sustain_ratio_as_u512; println!("foo {:?}", expected_total_reward); for request in post_genesis_requests { @@ -1804,18 +1805,55 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { 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 + let block_rewards = { + let mut ret = BTreeMap::new(); + ret.insert(VALIDATOR_1.clone(), vec![total_payout]); + ret + }; + let sustain_purse = URef::new([6u8; 32], AccessRights::READ_ADD_WRITE); + let block_rewards_request = builder.distribute_with_rewards_handling( + None, + ProtocolVersion::V2_0_0, + block_rewards.clone(), + 0, + RewardsHandling::Sustain { + ratio: Ratio::new(1, 2), + purse_address: sustain_purse.to_formatted_string(), }, - ) - .build(); + ); - builder.exec(distribute_request).commit().expect_success(); + // 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(); + println!("{:?}", block_rewards_request); + + 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( diff --git a/node/src/components/binary_port.rs b/node/src/components/binary_port.rs index 4e8a441bcd..69bf0d1073 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -192,6 +192,7 @@ async fn handle_request( config: &Config, metrics: &Metrics, protocol_version: ProtocolVersion, + chainspec: Arc, ) -> BinaryResponse where REv: From @@ -227,7 +228,15 @@ where try_speculative_execution(effect_builder, transaction).await } Command::Get(get_req) => { - handle_get_request(get_req, effect_builder, config, metrics, protocol_version).await + handle_get_request( + get_req, + effect_builder, + config, + metrics, + protocol_version, + chainspec, + ) + .await } } } @@ -238,6 +247,7 @@ async fn handle_get_request( config: &Config, metrics: &Metrics, protocol_version: ProtocolVersion, + chainspec: Arc, ) -> BinaryResponse where REv: From @@ -306,7 +316,9 @@ where return BinaryResponse::new_error(ErrorCode::UnsupportedRequest); }; match InformationRequest::try_from((tag, &key[..])) { - Ok(req) => handle_info_request(req, effect_builder, protocol_version).await, + Ok(req) => { + handle_info_request(req, effect_builder, protocol_version, chainspec).await + } Err(error) => { debug!(?tag, %error, "failed to parse an information request"); BinaryResponse::new_error(ErrorCode::MalformedInformationRequest) @@ -1010,6 +1022,7 @@ async fn handle_info_request( req: InformationRequest, effect_builder: EffectBuilder, protocol_version: ProtocolVersion, + chainspec: Arc, ) -> BinaryResponse where REv: From @@ -1232,12 +1245,14 @@ where let seigniorage_recipient = snapshot.get_seignorage_recipient(&header.era_id(), &validator); + let rewards_handling = chainspec.core_config.rewards_handling.clone(); let reward = auction::detail::reward( &validator, delegator.as_deref(), header.era_id(), validator_rewards, &snapshot, + rewards_handling, ); match (reward, seigniorage_recipient) { (Ok(Some(reward)), Some(seigniorage_recipient)) => { @@ -1846,6 +1861,7 @@ where let config = Arc::clone(&self.config); let metrics = Arc::clone(&self.metrics); let protocol_version = self.chainspec.protocol_version(); + let chainspec = Arc::clone(&self.chainspec); async move { let response = handle_request( request, @@ -1853,6 +1869,7 @@ where &config, &metrics, protocol_version, + chainspec, ) .await; responder.respond(response).await; diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index 8636667761..25de4dd6b0 100644 --- a/resources/local/chainspec.toml.in +++ b/resources/local/chainspec.toml.in @@ -172,7 +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 = 'sustain', ratio = [2, 100], purse_address = 'uref-addr' } +rewards_handling = { type = 'standard'} [highway] # Highway dynamically chooses its round length, between minimum_block_time and maximum_round_length. diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index 254b918a38..66bce4a250 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -439,7 +439,9 @@ pub trait CommitProvider: StateProvider { } }; - if let Err(auction_error) = runtime.distribute(rewards.clone()) { + let rewards_handling = request.config().rewards_handling(); + println!("In gs, rewards handling:{:?}", rewards_handling); + if let Err(auction_error) = runtime.distribute(rewards.clone(), rewards_handling) { error!( "distribute block rewards failed due to auction error {:?}", auction_error diff --git a/storage/src/system/auction.rs b/storage/src/system/auction.rs index 5c7d01c485..4efb64cd18 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,7 +633,11 @@ 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>, + rewards_handling: RewardsHandling, + ) -> Result<(), Error> { if self.get_caller() != PublicKey::System.to_account_hash() { error!("invalid caller to auction distribute"); return Err(Error::InvalidCaller); @@ -650,9 +654,25 @@ pub trait Auction: ret }; let total = Ratio::new(total, U512::one()); - let skim = Ratio::new(U512::from(50), U512::one()); + let skim = 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()); - let _share = (skim * total).to_integer(); + Ratio::new(numerator, denom) + } + }; + + let share = (skim * total).to_integer(); + + println!("{:?}", rewards_handling); + if let RewardsHandling::Sustain { purse_address, .. } = rewards_handling { + let purse_uref = + URef::from_formatted_str(&purse_address).map_err(|_| Error::Serialization)?; + println!("transferring {:?} to purse {:?}", share, purse_uref); + self.mint_into_existing_purse(share, purse_uref)?; + } debug!("reading seigniorage recipients snapshot"); let seigniorage_recipients_snapshot = detail::get_seigniorage_recipients_snapshot(self)?; @@ -671,6 +691,7 @@ pub trait Auction: current_era_id, &amounts, &SeigniorageRecipientsSnapshot::V2(seigniorage_recipients_snapshot.clone()), + skim, ) .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 c65d3a5d02..9870e368d7 100644 --- a/storage/src/system/auction/detail.rs +++ b/storage/src/system/auction/detail.rs @@ -14,7 +14,7 @@ use casper_types::{ AUCTION_DELAY_KEY, DELEGATION_RATE_DENOMINATOR, ERA_END_TIMESTAMP_MILLIS_KEY, ERA_ID_KEY, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, UNBONDING_DELAY_KEY, VALIDATOR_SLOTS_KEY, }, - AccessRights, ApiError, CLTyped, EraId, Key, KeyTag, PublicKey, URef, U512, + AccessRights, ApiError, CLTyped, EraId, Key, KeyTag, PublicKey, RewardsHandling, URef, U512, }; use num_rational::Ratio; use num_traits::{CheckedMul, CheckedSub}; @@ -1581,14 +1581,26 @@ pub fn reward( era_id: EraId, rewards: &[U512], seigniorage_recipients_snapshot: &SeigniorageRecipientsSnapshot, + rewards_handling: RewardsHandling, ) -> 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 = match rewards_handling { + RewardsHandling::Standard => Ratio::new(U512::zero(), U512::zero()), + RewardsHandling::Sustain { ratio, .. } => { + Ratio::new(U512::from(*ratio.numer()), U512::from(*ratio.denom())) + } + }; + let validator_rewards = match rewards_per_validator( + validator, + era_id, + rewards, + seigniorage_recipients_snapshot, + rewards_ratio, + ) { + 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 +1626,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()); @@ -1626,7 +1639,8 @@ pub(crate) fn rewards_per_validator( .filter(|(amount, eras_back)| !amount.is_zero() || *eras_back == 0) { println!("reward amount: {:?}", reward_amount); - let total_reward = Ratio::from(reward_amount) * Ratio::new(U512::from(98), U512::from(100)); + let factor = { Ratio::new(U512::from(100), U512::from(100)) - rewards_ratio }; + let total_reward = Ratio::from(reward_amount) * factor; println!("total reward: {:?}", total_reward); let rewarded_era = era_id .checked_sub(eras_back) 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 ccf7f7a148..b702e0c52c 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -62,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; pub use transaction_config::{ DeployConfig, TransactionConfig, TransactionLaneDefinition, TransactionV1Config, }; diff --git a/types/src/chainspec/rewards_handling.rs b/types/src/chainspec/rewards_handling.rs index 76efbea665..efa70eba7b 100644 --- a/types/src/chainspec/rewards_handling.rs +++ b/types/src/chainspec/rewards_handling.rs @@ -1,13 +1,12 @@ /// Configuration options of refund handling that are executed as part of handle payment /// finalization. use num_rational::Ratio; -use num_traits::Zero; use serde::{Deserialize, Serialize}; use crate::{ bytesrepr::{self, Error, FromBytes, ToBytes}, uref::FromStrError, - RefundHandling, URef, + URef, }; const REWARDS_HANDLING_STANDARD_TAG: u8 = 0; diff --git a/types/src/lib.rs b/types/src/lib.rs index ab8e405023..b9361db73f 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -130,9 +130,9 @@ 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, }; From 30483d8ceb439ce3edeb447205a1783d469ac78e Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Mon, 23 Feb 2026 11:31:34 -0600 Subject: [PATCH 04/12] Sustain test passing --- .../test/system_contracts/auction/distribute.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 ac8f7cb07c..165e1ec766 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 @@ -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, }, - AccessRights, EntityAddr, EraId, ProtocolVersion, PublicKey, RewardsHandling, SecretKey, - Timestamp, URef, DEFAULT_MINIMUM_BID_AMOUNT, U512, + AccessRights, CLValue, EntityAddr, EraId, Key, ProtocolVersion, PublicKey, RewardsHandling, + SecretKey, StoredValue, Timestamp, URef, DEFAULT_MINIMUM_BID_AMOUNT, U512, }; const ARG_ENTRY_POINT: &str = "entry_point"; @@ -1811,6 +1811,14 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { ret }; let sustain_purse = URef::new([6u8; 32], AccessRights::READ_ADD_WRITE); + builder.write_data_and_commit( + vec![( + Key::Balance([6u8; 32]), + StoredValue::CLValue(CLValue::from_t(U512::from(0)).unwrap()), + )] + .into_iter(), + ); + let block_rewards_request = builder.distribute_with_rewards_handling( None, ProtocolVersion::V2_0_0, @@ -1928,7 +1936,7 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { 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); + assert_eq!(delegator_2_updated_stake, delegator_2_expected_payout); let era_info = get_era_info(&mut builder); From f469bb218c09fb06b2068d4f9503196165697621 Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Thu, 26 Feb 2026 07:54:39 -0600 Subject: [PATCH 05/12] Sustain test passing pt.2 --- Makefile | 2 +- .../system_contracts/auction/distribute.rs | 60 +++++++++---------- resources/mainnet/chainspec.toml | 1 + resources/production/chainspec.toml | 2 +- storage/src/global_state/state/mod.rs | 1 - storage/src/system/auction.rs | 2 - storage/src/system/auction/detail.rs | 6 -- .../src/generic/state_tracker.rs | 1 - 8 files changed, 30 insertions(+), 45 deletions(-) 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_testing/tests/src/test/system_contracts/auction/distribute.rs b/execution_engine_testing/tests/src/test/system_contracts/auction/distribute.rs index 165e1ec766..6a468b05f0 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 @@ -14,7 +14,7 @@ use casper_engine_test_support::{ LOCAL_GENESIS_REQUEST, MINIMUM_ACCOUNT_CREATION_BALANCE, PRODUCTION_ROUND_SEIGNIORAGE_RATE, SYSTEM_ADDR, TIMESTAMP_MILLIS_INCREMENT, }; -use casper_storage::data_access_layer::{AuctionMethod, BlockRewardsRequest}; +use casper_storage::data_access_layer::AuctionMethod; use casper_types::{ self, account::AccountHash, @@ -1782,7 +1782,6 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { let sustain_ratio_as_u512 = Ratio::new(U512::from(1), U512::from(2)); let expected_total_reward = expected_total_reward * sustain_ratio_as_u512; - println!("foo {:?}", expected_total_reward); for request in post_genesis_requests { builder.exec(request).commit().expect_success(); @@ -1819,7 +1818,7 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { .into_iter(), ); - let block_rewards_request = builder.distribute_with_rewards_handling( + let block_rewards_result = builder.distribute_with_rewards_handling( None, ProtocolVersion::V2_0_0, block_rewards.clone(), @@ -1829,20 +1828,7 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { purse_address: sustain_purse.to_formatted_string(), }, ); - - // 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(); - println!("{:?}", block_rewards_request); + assert!(block_rewards_result.is_success()); let sustain_purse_expected_balance = { let total = { @@ -1879,8 +1865,6 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { delegator_reward.checked_sub(&commission).unwrap() }; - println!("share {:?}", delegators_share); - let delegator_1_expected_payout = { let reward_multiplier = Ratio::new( U512::from(DELEGATOR_1_STAKE), @@ -1905,9 +1889,6 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { let validator_1_expected_payout = { let total_delegator_payout = delegator_1_expected_payout + delegator_2_expected_payout; - println!( - "total delegator payout {delegator_1_expected_payout} {delegator_2_expected_payout}" - ); let validators_part = expected_total_reward - total_delegator_payout; validators_part.to_integer() @@ -2824,6 +2805,11 @@ fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_ 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(2)); + + let expected_total_reward = expected_total_reward * 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(); } @@ -2846,18 +2832,26 @@ fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_ rewards.insert(VALIDATOR_2.clone(), vec![total_payout]); rewards.insert(VALIDATOR_3.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(); + let sustain_purse = URef::new([6u8; 32], AccessRights::READ_ADD_WRITE); + builder.write_data_and_commit( + vec![( + Key::Balance([6u8; 32]), + StoredValue::CLValue(CLValue::from_t(U512::from(0)).unwrap()), + )] + .into_iter(), + ); - builder.exec(distribute_request).commit().expect_success(); + let block_rewards_result = builder.distribute_with_rewards_handling( + None, + ProtocolVersion::V2_0_0, + rewards.clone(), + 0, + RewardsHandling::Sustain { + ratio: Ratio::new(1, 2), + 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); 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 4367c1a5d7..5f8694cfe7 100644 --- a/resources/production/chainspec.toml +++ b/resources/production/chainspec.toml @@ -177,7 +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 -reward_handling = { type = 'sustain', ratio = [2, 100], purse_address = 'uref-addr' } +rewards_handling = { type = 'standard' } [highway] diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index 66bce4a250..d53795aae4 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -440,7 +440,6 @@ pub trait CommitProvider: StateProvider { }; let rewards_handling = request.config().rewards_handling(); - println!("In gs, rewards handling:{:?}", rewards_handling); if let Err(auction_error) = runtime.distribute(rewards.clone(), rewards_handling) { error!( "distribute block rewards failed due to auction error {:?}", diff --git a/storage/src/system/auction.rs b/storage/src/system/auction.rs index 4efb64cd18..8750781bd1 100644 --- a/storage/src/system/auction.rs +++ b/storage/src/system/auction.rs @@ -666,11 +666,9 @@ pub trait Auction: let share = (skim * total).to_integer(); - println!("{:?}", rewards_handling); if let RewardsHandling::Sustain { purse_address, .. } = rewards_handling { let purse_uref = URef::from_formatted_str(&purse_address).map_err(|_| Error::Serialization)?; - println!("transferring {:?} to purse {:?}", share, purse_uref); self.mint_into_existing_purse(share, purse_uref)?; } diff --git a/storage/src/system/auction/detail.rs b/storage/src/system/auction/detail.rs index 9870e368d7..b163a9a01a 100644 --- a/storage/src/system/auction/detail.rs +++ b/storage/src/system/auction/detail.rs @@ -1638,10 +1638,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) { - println!("reward amount: {:?}", reward_amount); let factor = { Ratio::new(U512::from(100), U512::from(100)) - rewards_ratio }; let total_reward = Ratio::from(reward_amount) * factor; - println!("total reward: {:?}", total_reward); let rewarded_era = era_id .checked_sub(eras_back) .ok_or(Error::MissingSeigniorageRecipients)?; @@ -1728,10 +1726,6 @@ pub(crate) fn rewards_per_validator( delegator_rewards.iter().map(|(_, &amount)| amount).sum(); let validator_reward = { total_reward - Ratio::from(total_delegator_payout) }.to_integer(); - println!( - "new rewards: {:?}, {total_delegator_payout}", - validator_reward - ); results.push(RewardsPerValidator { validator_reward, 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 From a6f4be6d6d5c6000bd440c5a80d01c210855f634 Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Thu, 26 Feb 2026 12:55:21 -0600 Subject: [PATCH 06/12] PR prep and cleanup --- .../system_contracts/auction/distribute.rs | 78 ++++++++++--------- node/src/reactor/main_reactor.rs | 11 ++- storage/src/system/auction.rs | 6 +- types/src/chainspec/rewards_handling.rs | 20 +++++ 4 files changed, 73 insertions(+), 42 deletions(-) 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 6a468b05f0..9cb2044a9b 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 @@ -1781,7 +1781,8 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { let sustain_ratio_as_u512 = Ratio::new(U512::from(1), U512::from(2)); - let expected_total_reward = expected_total_reward * sustain_ratio_as_u512; + 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(); @@ -2278,7 +2279,7 @@ fn should_distribute_with_multiple_validators_and_delegators() { #[ignore] #[test] -fn should_distribute_with_multiple_validators_and_shared_delegator() { +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; @@ -2451,6 +2452,12 @@ fn should_distribute_with_multiple_validators_and_shared_delegator() { 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(2)); + + 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(); } @@ -2473,18 +2480,26 @@ fn should_distribute_with_multiple_validators_and_shared_delegator() { rewards.insert(VALIDATOR_2.clone(), vec![total_payout]); rewards.insert(VALIDATOR_3.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(); + let sustain_purse = URef::new([6u8; 32], AccessRights::READ_ADD_WRITE); + builder.write_data_and_commit( + vec![( + Key::Balance([6u8; 32]), + StoredValue::CLValue(CLValue::from_t(U512::from(0)).unwrap()), + )] + .into_iter(), + ); - builder.exec(distribute_request).commit().expect_success(); + let block_rewards_result = builder.distribute_with_rewards_handling( + None, + ProtocolVersion::V2_0_0, + rewards.clone(), + 0, + RewardsHandling::Sustain { + ratio: Ratio::new(1, 2), + 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); @@ -2632,7 +2647,7 @@ fn should_distribute_with_multiple_validators_and_shared_delegator() { #[ignore] #[test] -fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_on() { +fn should_distribute_with_multiple_validators_and_shared_delegator() { 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; @@ -2805,11 +2820,6 @@ fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_ 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(2)); - - let expected_total_reward = expected_total_reward * 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(); } @@ -2832,26 +2842,18 @@ fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_ rewards.insert(VALIDATOR_2.clone(), vec![total_payout]); rewards.insert(VALIDATOR_3.clone(), vec![total_payout]); - let sustain_purse = URef::new([6u8; 32], AccessRights::READ_ADD_WRITE); - builder.write_data_and_commit( - vec![( - Key::Balance([6u8; 32]), - StoredValue::CLValue(CLValue::from_t(U512::from(0)).unwrap()), - )] - .into_iter(), - ); - - let block_rewards_result = builder.distribute_with_rewards_handling( - None, - ProtocolVersion::V2_0_0, - rewards.clone(), - 0, - RewardsHandling::Sustain { - ratio: Ratio::new(1, 2), - purse_address: sustain_purse.to_formatted_string(), + 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 }, - ); - assert!(block_rewards_result.is_success()); + ) + .build(); + + builder.exec(distribute_request).commit().expect_success(); let validator_1_delegator_1_share = { let total_reward = &Ratio::from(expected_total_reward_integer); 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/storage/src/system/auction.rs b/storage/src/system/auction.rs index 8750781bd1..6f137de3ee 100644 --- a/storage/src/system/auction.rs +++ b/storage/src/system/auction.rs @@ -654,7 +654,7 @@ pub trait Auction: ret }; let total = Ratio::new(total, U512::one()); - let skim = match rewards_handling { + let sustain_ratio = match rewards_handling { RewardsHandling::Standard => Ratio::new(U512::zero(), U512::one()), RewardsHandling::Sustain { ratio, .. } => { let numerator = U512::from(*ratio.numer()); @@ -664,7 +664,7 @@ pub trait Auction: } }; - let share = (skim * total).to_integer(); + let share = (sustain_ratio * total).to_integer(); if let RewardsHandling::Sustain { purse_address, .. } = rewards_handling { let purse_uref = @@ -689,7 +689,7 @@ pub trait Auction: current_era_id, &amounts, &SeigniorageRecipientsSnapshot::V2(seigniorage_recipients_snapshot.clone()), - skim, + sustain_ratio, ) .map(|infos| infos.into_iter().map(move |info| (proposer.clone(), info))) }) diff --git a/types/src/chainspec/rewards_handling.rs b/types/src/chainspec/rewards_handling.rs index efa70eba7b..f88772c857 100644 --- a/types/src/chainspec/rewards_handling.rs +++ b/types/src/chainspec/rewards_handling.rs @@ -39,6 +39,26 @@ impl RewardsHandling { 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 { From e1f96b37266ded036d69cb995c2872eadecc9e1c Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Fri, 27 Feb 2026 08:20:37 -0600 Subject: [PATCH 07/12] Fix math error --- storage/src/system/auction/detail.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage/src/system/auction/detail.rs b/storage/src/system/auction/detail.rs index b163a9a01a..37e5d001fa 100644 --- a/storage/src/system/auction/detail.rs +++ b/storage/src/system/auction/detail.rs @@ -1584,7 +1584,7 @@ pub fn reward( rewards_handling: RewardsHandling, ) -> Result, Error> { let rewards_ratio = match rewards_handling { - RewardsHandling::Standard => Ratio::new(U512::zero(), U512::zero()), + RewardsHandling::Standard => Ratio::new(U512::zero(), U512::one()), RewardsHandling::Sustain { ratio, .. } => { Ratio::new(U512::from(*ratio.numer()), U512::from(*ratio.denom())) } From b932c9dc8eca4e3d03108b7612d35eb17f40376f Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Fri, 27 Feb 2026 08:30:51 -0600 Subject: [PATCH 08/12] Update sustain ratio tests --- .../src/test/system_contracts/auction/distribute.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 9cb2044a9b..b8adc81af1 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 @@ -1678,7 +1678,7 @@ fn should_distribute_uneven_delegation_rate_zero() { 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 = 800_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; @@ -1779,7 +1779,7 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { 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(2)); + 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 }; @@ -1825,7 +1825,7 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { block_rewards.clone(), 0, RewardsHandling::Sustain { - ratio: Ratio::new(1, 2), + ratio: Ratio::new(1, 4), purse_address: sustain_purse.to_formatted_string(), }, ); @@ -2452,7 +2452,7 @@ fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_ 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(2)); + 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 }; @@ -2495,7 +2495,7 @@ fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_ rewards.clone(), 0, RewardsHandling::Sustain { - ratio: Ratio::new(1, 2), + ratio: Ratio::new(1, 4), purse_address: sustain_purse.to_formatted_string(), }, ); From a94bf219d723be1e81347ee56b88ff2650581188 Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Mon, 2 Mar 2026 08:44:32 -0600 Subject: [PATCH 09/12] Undo changes to binary port --- .../src/upgrade_request_builder.rs | 5 +- node/src/components/binary_port.rs | 27 +++------- .../seigniorage_recipients.rs | 4 ++ storage/src/global_state/state/mod.rs | 34 ++++++++++++- storage/src/system/auction/detail.rs | 16 +++--- storage/src/system/protocol_upgrade.rs | 38 +++++++++++++- types/src/chainspec.rs | 4 +- types/src/chainspec/rewards_handling.rs | 5 +- types/src/chainspec/upgrade_config.rs | 9 +++- types/src/key.rs | 49 ++++++++++++++++++- types/src/lib.rs | 2 +- 11 files changed, 154 insertions(+), 39 deletions(-) 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/node/src/components/binary_port.rs b/node/src/components/binary_port.rs index 69bf0d1073..8f6c0dfdb1 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -192,7 +192,6 @@ async fn handle_request( config: &Config, metrics: &Metrics, protocol_version: ProtocolVersion, - chainspec: Arc, ) -> BinaryResponse where REv: From @@ -228,15 +227,7 @@ where try_speculative_execution(effect_builder, transaction).await } Command::Get(get_req) => { - handle_get_request( - get_req, - effect_builder, - config, - metrics, - protocol_version, - chainspec, - ) - .await + handle_get_request(get_req, effect_builder, config, metrics, protocol_version).await } } } @@ -247,7 +238,6 @@ async fn handle_get_request( config: &Config, metrics: &Metrics, protocol_version: ProtocolVersion, - chainspec: Arc, ) -> BinaryResponse where REv: From @@ -316,9 +306,7 @@ where return BinaryResponse::new_error(ErrorCode::UnsupportedRequest); }; match InformationRequest::try_from((tag, &key[..])) { - Ok(req) => { - handle_info_request(req, effect_builder, protocol_version, chainspec).await - } + Ok(req) => handle_info_request(req, effect_builder, protocol_version).await, Err(error) => { debug!(?tag, %error, "failed to parse an information request"); BinaryResponse::new_error(ErrorCode::MalformedInformationRequest) @@ -1022,7 +1010,6 @@ async fn handle_info_request( req: InformationRequest, effect_builder: EffectBuilder, protocol_version: ProtocolVersion, - chainspec: Arc, ) -> BinaryResponse where REv: From @@ -1198,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) } @@ -1245,14 +1233,13 @@ where let seigniorage_recipient = snapshot.get_seignorage_recipient(&header.era_id(), &validator); - let rewards_handling = chainspec.core_config.rewards_handling.clone(); let reward = auction::detail::reward( &validator, delegator.as_deref(), header.era_id(), validator_rewards, &snapshot, - rewards_handling, + rewards_ratio, ); match (reward, seigniorage_recipient) { (Ok(Some(reward)), Some(seigniorage_recipient)) => { @@ -1861,7 +1848,6 @@ where let config = Arc::clone(&self.config); let metrics = Arc::clone(&self.metrics); let protocol_version = self.chainspec.protocol_version(); - let chainspec = Arc::clone(&self.chainspec); async move { let response = handle_request( request, @@ -1869,7 +1855,6 @@ where &config, &metrics, protocol_version, - chainspec, ) .await; responder.respond(response).await; 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 d53795aae4..ac7d1e0c79 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -38,7 +38,7 @@ use casper_types::{ }, 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)] @@ -1051,6 +1051,7 @@ pub trait StateProvider: Send + Sync + Sized { SeigniorageRecipientsResult::AuctionNotFound => EraValidatorsResult::AuctionNotFound, SeigniorageRecipientsResult::Success { seigniorage_recipients, + .. } => { let era_validators = match seigniorage_recipients { SeigniorageRecipientsSnapshot::V1(snapshot) => { @@ -2546,8 +2547,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/detail.rs b/storage/src/system/auction/detail.rs index 37e5d001fa..19a14099ea 100644 --- a/storage/src/system/auction/detail.rs +++ b/storage/src/system/auction/detail.rs @@ -14,7 +14,7 @@ use casper_types::{ AUCTION_DELAY_KEY, DELEGATION_RATE_DENOMINATOR, ERA_END_TIMESTAMP_MILLIS_KEY, ERA_ID_KEY, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, UNBONDING_DELAY_KEY, VALIDATOR_SLOTS_KEY, }, - AccessRights, ApiError, CLTyped, EraId, Key, KeyTag, PublicKey, RewardsHandling, URef, U512, + AccessRights, ApiError, CLTyped, EraId, Key, KeyTag, PublicKey, URef, U512, }; use num_rational::Ratio; use num_traits::{CheckedMul, CheckedSub}; @@ -1581,20 +1581,18 @@ pub fn reward( era_id: EraId, rewards: &[U512], seigniorage_recipients_snapshot: &SeigniorageRecipientsSnapshot, - rewards_handling: RewardsHandling, + rewards_ratio: Ratio, ) -> Result, Error> { - let rewards_ratio = match rewards_handling { - RewardsHandling::Standard => Ratio::new(U512::zero(), U512::one()), - RewardsHandling::Sustain { ratio, .. } => { - Ratio::new(U512::from(*ratio.numer()), U512::from(*ratio.denom())) - } - }; + 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, + rewards_ratio_as_u512, ) { Ok(rewards) => rewards, Err(Error::ValidatorNotFound) => return Ok(None), diff --git a/storage/src/system/protocol_upgrade.rs b/storage/src/system/protocol_upgrade.rs index 7b81f22b05..a16d4a4577 100644 --- a/storage/src/system/protocol_upgrade.rs +++ b/storage/src/system/protocol_upgrade.rs @@ -36,7 +36,8 @@ 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::{ @@ -217,6 +218,7 @@ where )?; self.handle_era_info_migration()?; self.handle_seignorage_snapshot_migration(system_entity_addresses.auction())?; + self.handle_rewards_handling()?; Ok(self.tracking_copy) } @@ -1568,6 +1570,40 @@ where Ok(()) } + /// Write or prune away the rewards handling entry in GS. + pub fn handle_rewards_handling(&mut self) -> 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, .. } => { + 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/types/src/chainspec.rs b/types/src/chainspec.rs index b702e0c52c..4b25f79c11 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -62,7 +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; +pub use rewards_handling::{RewardsHandling, REWARDS_HANDLING_RATIO_TAG}; pub use transaction_config::{ DeployConfig, TransactionConfig, TransactionLaneDefinition, TransactionV1Config, }; @@ -196,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, @@ -216,6 +217,7 @@ impl Chainspec { maximum_delegation_amount, minimum_delegation_amount, enable_addressable_entity, + rewards_handling, )) } diff --git a/types/src/chainspec/rewards_handling.rs b/types/src/chainspec/rewards_handling.rs index f88772c857..3c59cb4109 100644 --- a/types/src/chainspec/rewards_handling.rs +++ b/types/src/chainspec/rewards_handling.rs @@ -1,5 +1,4 @@ -/// Configuration options of refund handling that are executed as part of handle payment -/// finalization. +/// Configuration options of reward handling that are executed as part of rewards distribution. use num_rational::Ratio; use serde::{Deserialize, Serialize}; @@ -9,6 +8,8 @@ use crate::{ URef, }; +pub const REWARDS_HANDLING_RATIO_TAG: u8 = 0; + const REWARDS_HANDLING_STANDARD_TAG: u8 = 0; const REWARDS_HANDLING_SUSTAIN_TAG: u8 = 1; 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 b9361db73f..0b29fed24b 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -134,7 +134,7 @@ pub use chainspec::{ 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::{ From d812573a3e4af896bb7d83142fc153391a1ce832 Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Mon, 2 Mar 2026 09:45:37 -0600 Subject: [PATCH 10/12] amend faucet costs --- .../tests/src/test/explorer/faucet.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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]); From 1a1d529f61d6bea5fbf30f69240c18618eb34163 Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Mon, 2 Mar 2026 12:35:46 -0600 Subject: [PATCH 11/12] add support for sustain in genesis --- execution_engine/src/runtime/mod.rs | 35 ++++++++++-- .../src/genesis_config_builder.rs | 8 +++ .../test_support/src/lib.rs | 7 +++ .../system_contracts/auction/distribute.rs | 28 +++++----- storage/src/data_access_layer/genesis.rs | 6 ++ storage/src/global_state/state/mod.rs | 17 +++++- storage/src/system/auction.rs | 11 ++-- .../genesis/account_contract_installer.rs | 55 ++++++++++++++++--- .../src/chainspec/accounts_config/genesis.rs | 34 +++++++++++- types/src/chainspec/genesis_config.rs | 18 +++++- types/src/system/mint/constants.rs | 2 + 11 files changed, 185 insertions(+), 36 deletions(-) diff --git a/execution_engine/src/runtime/mod.rs b/execution_engine/src/runtime/mod.rs index c1cf22501e..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::{ @@ -1256,8 +1259,30 @@ where 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)?; + + 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, rewards_handling) + .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/tests/src/test/system_contracts/auction/distribute.rs b/execution_engine_testing/tests/src/test/system_contracts/auction/distribute.rs index b8adc81af1..91329fd7f1 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, }, - AccessRights, CLValue, EntityAddr, EraId, Key, ProtocolVersion, PublicKey, RewardsHandling, - SecretKey, StoredValue, Timestamp, URef, DEFAULT_MINIMUM_BID_AMOUNT, U512, + AccessRights, CLValue, EntityAddr, EraId, GenesisAccount, Key, ProtocolVersion, PublicKey, + RewardsHandling, SecretKey, StoredValue, Timestamp, URef, DEFAULT_MINIMUM_BID_AMOUNT, U512, }; const ARG_ENTRY_POINT: &str = "entry_point"; @@ -1769,7 +1769,12 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { let mut builder = LmdbWasmTestBuilder::default(); - builder.run_genesis(LOCAL_GENESIS_REQUEST.clone()); + 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 @@ -1810,14 +1815,11 @@ fn should_distribute_uneven_delegation_rate_zero_with_sustain_turned_on() { ret.insert(VALIDATOR_1.clone(), vec![total_payout]); ret }; - let sustain_purse = URef::new([6u8; 32], AccessRights::READ_ADD_WRITE); - builder.write_data_and_commit( - vec![( - Key::Balance([6u8; 32]), - StoredValue::CLValue(CLValue::from_t(U512::from(0)).unwrap()), - )] - .into_iter(), - ); + + 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, 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/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index ac7d1e0c79..9c280f3b1a 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -31,8 +31,8 @@ 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, }, @@ -440,7 +440,18 @@ pub trait CommitProvider: StateProvider { }; let rewards_handling = request.config().rewards_handling(); - if let Err(auction_error) = runtime.distribute(rewards.clone(), 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 diff --git a/storage/src/system/auction.rs b/storage/src/system/auction.rs index 6f137de3ee..ad9bf4ecaf 100644 --- a/storage/src/system/auction.rs +++ b/storage/src/system/auction.rs @@ -636,6 +636,7 @@ pub trait Auction: fn distribute( &mut self, rewards: BTreeMap>, + sustain_purse: Option, rewards_handling: RewardsHandling, ) -> Result<(), Error> { if self.get_caller() != PublicKey::System.to_account_hash() { @@ -666,10 +667,12 @@ pub trait Auction: let share = (sustain_ratio * total).to_integer(); - if let RewardsHandling::Sustain { purse_address, .. } = rewards_handling { - let purse_uref = - URef::from_formatted_str(&purse_address).map_err(|_| Error::Serialization)?; - self.mint_into_existing_purse(share, purse_uref)?; + 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"); 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/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/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/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"; From e2936e28da4f3e12317d1e7e05c7c6a45615a3b9 Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Mon, 2 Mar 2026 12:55:26 -0600 Subject: [PATCH 12/12] Amend tests for change in genesis --- .../system_contracts/auction/distribute.rs | 23 +++++----- storage/src/system/protocol_upgrade.rs | 42 ++++++++++++++++--- 2 files changed, 48 insertions(+), 17 deletions(-) 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 91329fd7f1..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 @@ -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, }, - AccessRights, CLValue, EntityAddr, EraId, GenesisAccount, Key, ProtocolVersion, PublicKey, - RewardsHandling, SecretKey, StoredValue, Timestamp, URef, 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"; @@ -2444,7 +2444,12 @@ fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_ let mut builder = LmdbWasmTestBuilder::default(); - builder.run_genesis(LOCAL_GENESIS_REQUEST.clone()); + 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 @@ -2482,14 +2487,10 @@ fn should_distribute_with_multiple_validators_and_shared_delegator_with_sustain_ rewards.insert(VALIDATOR_2.clone(), vec![total_payout]); rewards.insert(VALIDATOR_3.clone(), vec![total_payout]); - let sustain_purse = URef::new([6u8; 32], AccessRights::READ_ADD_WRITE); - builder.write_data_and_commit( - vec![( - Key::Balance([6u8; 32]), - StoredValue::CLValue(CLValue::from_t(U512::from(0)).unwrap()), - )] - .into_iter(), - ); + 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, diff --git a/storage/src/system/protocol_upgrade.rs b/storage/src/system/protocol_upgrade.rs index a16d4a4577..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, }, @@ -42,7 +42,7 @@ use casper_types::{ use crate::{ global_state::state::StateProvider, - tracking_copy::{TrackingCopy, TrackingCopyEntityExt, TrackingCopyExt}, + tracking_copy::{AddResult, TrackingCopy, TrackingCopyEntityExt, TrackingCopyExt}, AddressGenerator, }; @@ -218,7 +218,7 @@ where )?; self.handle_era_info_migration()?; self.handle_seignorage_snapshot_migration(system_entity_addresses.auction())?; - self.handle_rewards_handling()?; + self.handle_rewards_handling(system_entity_addresses.mint())?; Ok(self.tracking_copy) } @@ -1571,7 +1571,7 @@ where } /// Write or prune away the rewards handling entry in GS. - pub fn handle_rewards_handling(&mut self) -> Result<(), ProtocolUpgradeError> { + 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 @@ -1584,7 +1584,37 @@ where self.tracking_copy.prune(Key::RewardsHandling); } } - RewardsHandling::Sustain { ratio, .. } => { + 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()))?;