diff --git a/Cargo.lock b/Cargo.lock index 595c615d..c1106105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,7 @@ dependencies = [ "caryatid_sdk", "chrono", "config", + "csv", "dashmap", "hex", "imbl 5.0.0", @@ -1631,6 +1632,27 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "382ce8820a5bb815055d3553a610e8cb542b2d767bbacea99038afda96cd760d" +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" diff --git a/common/src/ledger_state.rs b/common/src/ledger_state.rs index 87187913..998e6d63 100644 --- a/common/src/ledger_state.rs +++ b/common/src/ledger_state.rs @@ -33,6 +33,8 @@ pub struct SPOState { #[n(0)] pub pools: BTreeMap, #[n(1)] + pub updates: BTreeMap, + #[n(2)] pub retiring: BTreeMap, } diff --git a/modules/accounts_state/Cargo.toml b/modules/accounts_state/Cargo.toml index 50a93085..cbe32223 100644 --- a/modules/accounts_state/Cargo.toml +++ b/modules/accounts_state/Cargo.toml @@ -24,6 +24,8 @@ bigdecimal = "0.4.8" rayon = "1.10.0" dashmap = "6.1.0" chrono = "0.4.41" +csv = "1.3.1" +itertools = "0.14.0" [lib] path = "src/accounts_state.rs" diff --git a/modules/accounts_state/NOTES.md b/modules/accounts_state/NOTES.md index f33c7d8d..c5ef88b4 100644 --- a/modules/accounts_state/NOTES.md +++ b/modules/accounts_state/NOTES.md @@ -80,7 +80,7 @@ ORDER BY pool_id_hex; ## Specific SPO test -Epoch 212 (spendable in 213), SPO 30c6319d1f680..., rewards actually given out: +Epoch 211 (spendable in 213), SPO 30c6319d1f680..., rewards actually given out: ```sql SELECT @@ -94,24 +94,45 @@ AND encode(ph.hash_raw, 'hex') LIKE '30c6319d1f680%' GROUP BY ph.hash_raw; ``` +Note: pool_id for this SPO is 93 + | pool_id_hex | member_rewards | leader_rewards | |----------------------------------------------------------|----------------|----------------| -| 30c6319d1f680470c8d2d48f8d44fd2848fa9b8cd6ac944d4dfc0c54 | 33869550293 | 2164196243 | +| 30c6319d1f680470c8d2d48f8d44fd2848fa9b8cd6ac944d4dfc0c54 | 32024424770 | 2067130351 | Total 34091555121 We have ``` -2025-08-21T13:59:50.578627Z INFO acropolis_module_accounts_state::rewards: Pool 30c6319d1f680470c8d2d48f8d44fd2848fa9b8cd6ac944d4dfc0c54 blocks=1 pool_stake=44180895641393 relative_pool_stake=0.001392062719472796345022132114111547444335115561171699064775592918376184270138741760710696148952284469 relative_blocks=0.0005022601707684580612757408337518834756403817177297840281265695630336514314414866901054746358613761929 pool_performance=1 optimum_rewards=34113076193 pool_rewards=34113076193 +2025-08-26T10:49:39.003335Z INFO acropolis_module_accounts_state::rewards: Pool 30c6319d1f680470c8d2d48f8d44fd2848fa9b8cd6ac944d4dfc0c54 blocks=0 pool_stake=44180895641393 relative_pool_stake=0.001392062719472796345022132114111547444335115561171699064775592918376184270 +138741760710696148952284469 relative_blocks=0 pool_performance=1 optimum_rewards=34091555158 pool_rewards=34091555158 ``` -Optimum rewards: 34113076193 +Optimum rewards: 34091555158 + +Difference: We are too high by 37 LL compared to DBSync - suspect rounding of individual payments +We match the maxP from the Haskell node: + +``` +**** Calculating PoolRewardInfo: epoch=0, rewardInfo=PoolRewardInfo {poolRelativeStake = StakeShare (44180895641393 % 31737719158318701), poolPot = Coin 34091555158, poolPs = PoolParams {ppId = KeyHash {unKeyHash = "30c6319d1f680470c8d2d48f8d44fd2848fa9b8cd6ac944d4dfc0c54"}, ppVrf = VRFVerKeyHash {unVRFVerKeyHash = "f2b08e8ec5fe945b41ece1c254e25843e35e574dd43535cbf244524019f704e9"}, ppPledge = Coin 50000000000, ppCost = Coin 340000000, ppMargin = 1 % 20, ppRewardAccount = RewardAccount {raNetwork = Mainnet, raCredential = KeyHashObj (KeyHash {unKeyHash = "8a10720c17ce32b75f489ed13fb706dac51c6006b7fee1a687f36620"})}, ppOwners = fromList [KeyHash {unKeyHash = "8a10720c17ce32b75f489ed13fb706dac51c6006b7fee1a687f36620"}], ppRelays = StrictSeq {fromStrict = fromList [SingleHostName (SJust (Port {portToWord16 = 3001})) (DnsName {dnsToText = "europe1-relay.jpn-sp.net"})]}, ppMetadata = SJust (PoolMetadata {pmUrl = Url {urlToText = "https://tokyostaker.com/metadata/jp3.json"}, pmHash = "\201\246\183K\128\&1 \EOT*\f\194\GS>B\168\136j\239\241\&4\189\230\175\SI4\163\160P\206\162\163]"})}, poolBlocks = 1, poolLeaderReward = LeaderOnlyReward {lRewardPool = KeyHash {unKeyHash = "30c6319d1f680470c8d2d48f8d44fd2848fa9b8cd6ac944d4dfc0c54"}, lRewardAmount = Coin 2067130351}}, activeStake=Coin 10177811974822904, totalStake=Coin 31737719158318701, pledgeRelative=50000000000 % 31737719158318701, sigmaA=44180895641393 % 10177811974822904, maxP=34091555158, appPerf=1 % 1, R=Coin 31834688329017**** +``` -Difference: We are too high by 21521072, or 0.06% +## ADA pots data from DBSync -Input into this in epoch 212 is: +First 10 epochs in ada_pots: ``` -Calculating rewards: epoch=212 total_supply=31737719158318701 stake_rewards=31854784667376 +id | slot_no | epoch_no | treasury | reserves | rewards | utxo | deposits_stake | fees | block_id | deposits_drep | deposits_proposal +-----+-----------+----------+------------------+-------------------+-----------------+-------------------+----------------+--------------+----------+---------------+------------------- + 1 | 4924800 | 209 | 8332813711755 | 13286160713028443 | 593536826186446 | 31111517964861148 | 441012000000 | 10670212208 | 4512244 | 0 | 0 + 2 | 5356800 | 210 | 16306644182013 | 13278197552770393 | 277915861250199 | 31427038405450971 | 533870000000 | 7666346424 | 4533814 | 0 | 0 + 3 | 5788800 | 211 | 24275595982960 | 13270236767315870 | 164918966125973 | 31539966264042924 | 594636000000 | 7770532273 | 4555361 | 0 | 0 + 4 | 6220800 | 212 | 32239292149804 | 13262280841681299 | 147882943225525 | 31556964153057144 | 626252000000 | 6517886228 | 4576676 | 0 | 0 + 5 | 6652800 | 213 | 40198464232058 | 13247093198353459 | 133110645284460 | 31578940375911744 | 651738000000 | 5578218279 | 4597956 | 0 | 0 + 6 | 7084800 | 214 | 48148335794725 | 13230232787944838 | 121337581585558 | 31599599756081623 | 674438000000 | 7100593256 | 4619398 | 0 | 0 + 7 | 7516800 | 215 | 55876297807656 | 13212986170770203 | 117660526059600 | 31612774463528795 | 695040000000 | 7501833746 | 4640850 | 0 | 0 + 8 | 7948807 | 216 | 63707722011028 | 13195031638588164 | 122159720478561 | 31618386634872973 | 706174000000 | 8110049274 | 4662422 | 0 | 0 + 9 | 8380800 | 217 | 71629614335572 | 13176528835451373 | 127730158329564 | 31623386398075064 | 719058000000 | 5935808427 | 4683639 | 0 | 0 + 10 | 8812800 | 218 | 79429791062499 | 13157936081322000 | 134680552513121 | 31627219255406326 | 729244000000 | 5075696054 | 4704367 | 0 | 0 ``` diff --git a/modules/accounts_state/README.md b/modules/accounts_state/README.md new file mode 100644 index 00000000..d2a5f640 --- /dev/null +++ b/modules/accounts_state/README.md @@ -0,0 +1,62 @@ +# AccountsState module + +This is the module which does the majority of the work in calculating monetary change +(reserves, treasury) and rewards + +## Notes on verification + +The module has an inbuilt 'Verifier' which can compare against CSV files dumped from +DBSync. + +### Pots verification + +Verifying the 'pots' values (reserves, treasury, deposits) is a good overall marker of +successful calculation since everything (including rewards) feeds into it. + +To create a pots verification file, export the `ada_pots` table as CSV +from Postgres on a DBSync database: + +```sql +\COPY ( + SELECT epoch_no AS epoch, reserves, treasury, deposits_stake AS deposits + FROM ada_pots + ORDER BY epoch_no +) TO 'pots.mainnet.csv' WITH CSV HEADER +``` + +Then configure this as (e.g.) + +```toml +[module.accounts-state] +verify-pots-file = "../../modules/accounts_state/test-data/pots.mainnet.csv" +``` + +This is the default, since the pots file is small. It will be updated periodically. + +### Rewards verification + +The verifier can also compare the rewards paid to members (delegators) and leader (pool) +against a capture from the DBSync `rewards` table. We name the files for the epoch *earned*, +which is one less than when we calculate it. + +To create a rewards CSV file in Postgres on a DBSync database: + +```sql +\COPY ( + select encode(ph.hash_raw, 'hex') as spo, encode(a.hash_raw, 'hex') as address, + r.type, r.amount + from reward r + join pool_hash ph on r.pool_id = ph.id + join stake_address a on a.id = r.addr_id + where r.earned_epoch=211 and r.amount > 0 +) to 'rewards.mainnet.211.csv' with csv header +``` + +To configure verification, provide a path template which takes the epoch number: + +```toml +[module.accounts-state] +verify-rewards-files = "../../modules/accounts_state/test-data/rewards.mainnet.{}.csv" +``` + +The verifier will only verify epochs where this file exists. diff --git a/modules/accounts_state/src/accounts_state.rs b/modules/accounts_state/src/accounts_state.rs index 003b8072..ae7face8 100644 --- a/modules/accounts_state/src/accounts_state.rs +++ b/modules/accounts_state/src/accounts_state.rs @@ -25,9 +25,11 @@ use state::State; mod monetary; mod rewards; mod snapshot; +mod verifier; use acropolis_common::queries::accounts::{ AccountInfo, AccountsStateQuery, AccountsStateQueryResponse, }; +use verifier::Verifier; const DEFAULT_SPO_STATE_TOPIC: &str = "cardano.spo.state"; const DEFAULT_EPOCH_ACTIVITY_TOPIC: &str = "cardano.epoch.activity"; @@ -64,6 +66,7 @@ impl AccountsState { mut stake_subscription: Box>, mut drep_state_subscription: Box>, mut parameters_subscription: Box>, + verifier: &Verifier, ) -> Result<()> { // Get the stake address deltas from the genesis bootstrap, which we know // don't contain any stake, plus an extra parameter state (!unexplained) @@ -181,30 +184,30 @@ impl AccountsState { _ => error!("Unexpected message type: {message:?}"), } - // Handle epoch activity - let (_, message) = ea_message_f.await?; + // Update parameters, ready for monetary/rewards calc triggered by epoch_activity + let (_, message) = params_message_f.await?; match message.as_ref() { - Message::Cardano((block_info, CardanoMessage::EpochActivity(ea_msg))) => { + Message::Cardano((block_info, CardanoMessage::ProtocolParams(params_msg))) => { let span = info_span!( - "account_state.handle_epoch_activity", + "account_state.handle_parameters", block = block_info.number ); async { Self::check_sync(¤t_block, &block_info); - let spo_rewards = state - .handle_epoch_activity(ea_msg) - .await - .inspect_err(|e| error!("EpochActivity handling error: {e:#}")) - .ok(); - // SPO rewards is for previous epoch - if let Some(spo_rewards) = spo_rewards { - if let Err(e) = spo_rewards_publisher - .publish_spo_rewards(block_info, spo_rewards) - .await - { - error!("Error publishing SPO rewards: {e:#}") + if let Some(ref block) = current_block { + if block.number != block_info.number { + error!( + expected = block.number, + received = block_info.number, + "Certificate and parameters messages re-ordered!" + ); } } + + state + .handle_parameters(params_msg) + .inspect_err(|e| error!("Messaging handling error: {e}")) + .ok(); } .instrument(span) .await; @@ -213,31 +216,30 @@ impl AccountsState { _ => error!("Unexpected message type: {message:?}"), } - // Update parameters - *after* reward calculation in epoch-activity above - // ready for the *next* epoch boundary - let (_, message) = params_message_f.await?; + // Handle epoch activity + let (_, message) = ea_message_f.await?; match message.as_ref() { - Message::Cardano((block_info, CardanoMessage::ProtocolParams(params_msg))) => { + Message::Cardano((block_info, CardanoMessage::EpochActivity(ea_msg))) => { let span = info_span!( - "account_state.handle_parameters", + "account_state.handle_epoch_activity", block = block_info.number ); async { Self::check_sync(¤t_block, &block_info); - if let Some(ref block) = current_block { - if block.number != block_info.number { - error!( - expected = block.number, - received = block_info.number, - "Certificate and parameters messages re-ordered!" - ); + let spo_rewards = state + .handle_epoch_activity(ea_msg, &verifier) + .await + .inspect_err(|e| error!("EpochActivity handling error: {e:#}")) + .ok(); + // SPO rewards is for previous epoch + if let Some(spo_rewards) = spo_rewards { + if let Err(e) = spo_rewards_publisher + .publish_spo_rewards(block_info, spo_rewards) + .await + { + error!("Error publishing SPO rewards: {e:#}") } } - - state - .handle_parameters(params_msg) - .inspect_err(|e| error!("Messaging handling error: {e}")) - .ok(); } .instrument(span) .await; @@ -388,6 +390,19 @@ impl AccountsState { .unwrap_or(DEFAULT_ACCOUNTS_QUERY_TOPIC.1.to_string()); info!("Creating query handler on '{}'", accounts_query_topic); + // Create verifier and read comparison data according to config + let mut verifier = Verifier::new(); + + if let Ok(verify_pots_file) = config.get_string("verify-pots-file") { + info!("Verifying pots against '{verify_pots_file}'"); + verifier.read_pots(&verify_pots_file); + } + + if let Ok(verify_rewards_files) = config.get_string("verify-rewards-files") { + info!("Verifying rewards against '{verify_rewards_files}'"); + verifier.read_rewards(&verify_rewards_files); + } + // Create history let history = Arc::new(Mutex::new(StateHistory::::new( "AccountsState", @@ -529,6 +544,7 @@ impl AccountsState { stake_subscription, drep_state_subscription, parameters_subscription, + &verifier, ) .await .unwrap_or_else(|e| error!("Failed: {e}")); diff --git a/modules/accounts_state/src/monetary.rs b/modules/accounts_state/src/monetary.rs index eeb16d76..1a978565 100644 --- a/modules/accounts_state/src/monetary.rs +++ b/modules/accounts_state/src/monetary.rs @@ -13,7 +13,7 @@ pub struct MonetaryResult { pub pots: Pots, /// Total stake reward available - pub stake_rewards: BigDecimal, + pub stake_rewards: Lovelace, } /// Calculate monetary change at the start of an epoch, returning updated pots and total @@ -39,8 +39,7 @@ pub fn calculate_monetary_change( let total_reward_pot = &monetary_expansion + BigDecimal::from(total_fees_last_epoch); // Top-slice some for treasury - let treasury_cut = RationalNumber::new(2, 10); - // TODO odd values again! ¶ms.protocol_params.treasury_cut; // Tau + let treasury_cut = ¶ms.protocol_params.treasury_cut; // Tau let treasury_increase = (&total_reward_pot * BigDecimal::from(treasury_cut.numer()) / BigDecimal::from(treasury_cut.denom())) .with_scale(0); @@ -52,10 +51,12 @@ pub fn calculate_monetary_change( new_pots.reserves -= treasury_increase_u64; // Remainder goes to stakeholders - let stake_rewards = &total_reward_pot - &treasury_increase; + let stake_rewards = (&total_reward_pot - &treasury_increase) + .to_u64() + .ok_or(anyhow!("Can't calculate integral stake rewards"))?; info!(total_rewards=%total_reward_pot, cut=%treasury_cut, increase=treasury_increase_u64, - %stake_rewards, "Treasury:"); + stake_rewards, "Treasury:"); Ok(MonetaryResult { pots: new_pots, @@ -115,18 +116,26 @@ mod tests { // Known values at start of Shelley - from Java reference and DBSync const EPOCH_208_RESERVES: Lovelace = 13_888_022_852_926_644; const EPOCH_208_MIRS: Lovelace = 593_529_326_186_446; - const EPOCH_208_FEES: Lovelace = 10_670_212_208; const EPOCH_209_RESERVES: Lovelace = 13_286_160_713_028_443; const EPOCH_209_TREASURY: Lovelace = 8_332_813_711_755; - const EPOCH_209_FEES: Lovelace = 7_666_346_424; + const EPOCH_209_FEES: Lovelace = 10_670_212_208; + const EPOCH_209_STAKE_REWARDS: Lovelace = 33_331_254_847_024; const EPOCH_210_RESERVES: Lovelace = 13_278_197_552_770_393; const EPOCH_210_TREASURY: Lovelace = 16_306_644_182_013; const EPOCH_210_REFUNDS_TO_TREASURY: Lovelace = 500_000_000; // 1 SPO with unknown reward + const EPOCH_210_FEES: Lovelace = 7_666_346_424; + const EPOCH_210_STAKE_REWARDS: Lovelace = 31_895_321_881_035; const EPOCH_211_RESERVES: Lovelace = 13_270_236_767_315_870; const EPOCH_211_TREASURY: Lovelace = 24_275_595_982_960; + const EPOCH_211_FEES: Lovelace = 7_770_532_273; + const EPOCH_211_STAKE_REWARDS: Lovelace = 31_873_807_203_788; + + const EPOCH_212_RESERVES: Lovelace = 13_262_280_841_681_299; + const EPOCH_212_TREASURY: Lovelace = 32_239_292_149_804; + const EPOCH_212_STAKE_REWARDS: Lovelace = 31_854_784_667_376; fn shelley_params() -> ShelleyParams { ShelleyParams { @@ -167,7 +176,7 @@ mod tests { } #[test] - fn epoch_208_monetary_change() { + fn epoch_209_monetary_change() { let params = shelley_params(); let pots = Pots { reserves: EPOCH_208_RESERVES, @@ -185,10 +194,11 @@ mod tests { ); assert_eq!(result.pots.reserves - EPOCH_208_MIRS, EPOCH_209_RESERVES); assert_eq!(result.pots.treasury, EPOCH_209_TREASURY); + assert_eq!(result.stake_rewards, EPOCH_209_STAKE_REWARDS); } #[test] - fn epoch_209_monetary_change() { + fn epoch_210_monetary_change() { let params = shelley_params(); let pots = Pots { reserves: EPOCH_209_RESERVES, @@ -197,15 +207,16 @@ mod tests { }; // Epoch 208 had no non-OBFT blocks - let result = calculate_monetary_change(¶ms, &pots, EPOCH_208_FEES, 0).unwrap(); + let result = calculate_monetary_change(¶ms, &pots, EPOCH_209_FEES, 0).unwrap(); - // Epoch 210 reserves + // Epoch 210 results assert_eq!(result.pots.reserves, EPOCH_210_RESERVES); assert_eq!(result.pots.treasury, EPOCH_210_TREASURY); + assert_eq!(result.stake_rewards, EPOCH_210_STAKE_REWARDS); } #[test] - fn epoch_210_monetary_change() { + fn epoch_211_monetary_change() { let params = shelley_params(); let pots = Pots { reserves: EPOCH_210_RESERVES, @@ -214,13 +225,32 @@ mod tests { }; // Epoch 209 had no non-OBFT blocks - let result = calculate_monetary_change(¶ms, &pots, EPOCH_209_FEES, 0).unwrap(); + let result = calculate_monetary_change(¶ms, &pots, EPOCH_210_FEES, 0).unwrap(); - // Epoch 211 reserves + // Epoch 211 results assert_eq!(result.pots.reserves, EPOCH_211_RESERVES); assert_eq!( result.pots.treasury + EPOCH_210_REFUNDS_TO_TREASURY, EPOCH_211_TREASURY ); + assert_eq!(result.stake_rewards, EPOCH_211_STAKE_REWARDS); + } + + #[test] + fn epoch_212_monetary_change() { + let params = shelley_params(); + let pots = Pots { + reserves: EPOCH_211_RESERVES, + treasury: EPOCH_211_TREASURY, + deposits: 0, + }; + + // Epoch 210 had OBFT blocks but the number is ignored because d=1.0 + let result = calculate_monetary_change(¶ms, &pots, EPOCH_211_FEES, 42).unwrap(); + + // Epoch 212 results + assert_eq!(result.pots.reserves, EPOCH_212_RESERVES); + assert_eq!(result.pots.treasury, EPOCH_212_TREASURY); + assert_eq!(result.stake_rewards, EPOCH_212_STAKE_REWARDS); } } diff --git a/modules/accounts_state/src/rewards.rs b/modules/accounts_state/src/rewards.rs index 054d0577..332485e3 100644 --- a/modules/accounts_state/src/rewards.rs +++ b/modules/accounts_state/src/rewards.rs @@ -8,250 +8,333 @@ use acropolis_common::{ use anyhow::{bail, Result}; use bigdecimal::{BigDecimal, One, ToPrimitive, Zero}; use std::cmp::min; +use std::collections::BTreeMap; use std::sync::Arc; -use tracing::{debug, info, warn}; +use tracing::{debug, info}; + +/// Type of reward being given +#[derive(Debug, Clone)] +pub enum RewardType { + Leader, + Member, +} + +/// Reward Detail +#[derive(Debug, Clone)] +pub struct RewardDetail { + /// Account reward paid to + pub account: RewardAccount, + + /// Type of reward + pub rtype: RewardType, + + /// Reward amount + pub amount: Lovelace, +} /// Result of a rewards calculation #[derive(Debug, Default)] pub struct RewardsResult { + /// Epoch these rewards were earned in (when blocks produced) + pub epoch: u64, + /// Total rewards paid pub total_paid: u64, /// Rewards to be paid - pub rewards: Vec<(RewardAccount, Lovelace)>, + pub rewards: BTreeMap>, /// SPO rewards pub spo_rewards: Vec<(KeyHash, SPORewards)>, } -/// State for rewards calculation -#[derive(Debug, Default, Clone)] -pub struct RewardsState { - /// Latest snapshot (epoch i) (if any) - pub mark: Arc, +/// Calculate rewards for a given epoch based on current rewards state and protocol parameters +/// The epoch is the one we are now entering - we assume the snapshot for this has already been +/// taken. +/// Note immutable - only state change allowed is to push a new snapshot +pub fn calculate_rewards( + epoch: u64, + performance: Arc, + staking: Arc, + params: &ShelleyParams, + stake_rewards: Lovelace, +) -> Result { + let mut result = RewardsResult::default(); + result.epoch = epoch - 1; + + // If no blocks produced in previous epoch, don't do anything + let total_blocks = performance.blocks; + if total_blocks == 0 { + return Ok(result); + } - /// Previous snapshot (epoch i-1) (if any) - pub set: Arc, + // Take stake rewards from epoch we just left + let stake_rewards = BigDecimal::from(stake_rewards); - /// One before that (epoch i-2) (if any) - pub go: Arc, -} + // Calculate total supply (total in circulation + treasury) or + // equivalently (max-supply-reserves) - this is the denominator + // for sigma, z0, s + let total_supply = BigDecimal::from(params.max_lovelace_supply - performance.pots.reserves); + info!(epoch, %total_supply, %stake_rewards, total_blocks, "Calculating rewards:"); -impl RewardsState { - /// Push a new snapshot - pub fn push(&mut self, latest: Snapshot) { - self.go = self.set.clone(); - self.set = self.mark.clone(); - self.mark = Arc::new(latest); + // Relative pool saturation size (z0) + let k = BigDecimal::from(¶ms.protocol_params.stake_pool_target_num); + if k.is_zero() { + bail!("k is zero!"); } - - /// Calculate rewards for a given epoch based on current rewards state and protocol parameters - /// The epoch is the one we are now entering - we assume the snapshot for this has already been - /// taken. - /// Note immutable - only state change allowed is to push a new snapshot - pub fn calculate_rewards( - &self, - epoch: u64, - params: &ShelleyParams, - total_blocks: usize, - stake_rewards: BigDecimal, - ) -> Result { - let mut result = RewardsResult::default(); - - // Calculate total supply (total in circulation + treasury) or - // equivalently (max-supply-reserves) - this is the denominator - // for sigma, z0, s - let total_supply = BigDecimal::from(params.max_lovelace_supply - self.mark.pots.reserves); - info!(epoch, %total_supply, %stake_rewards, "Calculating rewards:"); - - // Relative pool saturation size (z0) - let k = BigDecimal::from(¶ms.protocol_params.stake_pool_target_num); - if k.is_zero() { - bail!("k is zero!"); + let relative_pool_saturation_size = k.inverse(); + + // Pledge influence factor (a0) + let a0 = ¶ms.protocol_params.pool_pledge_influence; + let pledge_influence_factor = BigDecimal::from(a0.numer()) / BigDecimal::from(a0.denom()); + + // Calculate for every registered SPO (even those who didn't participate in this epoch) + // from epoch (i-2) "Go" + let mut total_paid_to_pools: Lovelace = 0; + let mut total_paid_to_delegators: Lovelace = 0; + let mut num_pools_paid: usize = 0; + let mut num_delegators_paid: usize = 0; + for (operator_id, spo) in staking.spos.iter() { + // Actual blocks produced for epoch i, no rewards if none + let performance_spo = performance.spos.get(operator_id); + let blocks_produced = performance_spo.map(|s| s.blocks_produced).unwrap_or(0); + if blocks_produced == 0 { + continue; } - let relative_pool_saturation_size = k.inverse(); - - // Pledge influence factor (a0) - let a0 = ¶ms.protocol_params.pool_pledge_influence; - let pledge_influence_factor = BigDecimal::from(a0.numer()) / BigDecimal::from(a0.denom()); - - // Calculate for every registered SPO (even those who didn't participate in this epoch) - // from epoch (i-2) "Go" - let mut total_paid_to_pools: Lovelace = 0; - let mut total_paid_to_delegators: Lovelace = 0; - let mut num_delegators_paid: usize = 0; - for (operator_id, spo) in self.go.spos.iter() { - // Actual blocks produced for epoch (i) - let blocks_produced = { - if let Some(s) = self.mark.spos.get(operator_id) { - s.blocks_produced - } else { - 0 - } + + // We get the registration status *as it is in performance* for the reward account + // *as it was during staking* + let staking_reward_account_is_registered = + performance_spo.map(|s| s.two_previous_reward_account_is_registered).unwrap_or(false); + + let rewards = calculate_spo_rewards( + operator_id, + spo, + blocks_produced as u64, + total_blocks, + &stake_rewards, + &total_supply, + &relative_pool_saturation_size, + &pledge_influence_factor, + params, + staking.clone(), + staking_reward_account_is_registered, + ); + + if !rewards.is_empty() { + num_pools_paid += 1; + + let mut spo_rewards = SPORewards { + total_rewards: 0, + operator_rewards: 0, }; + for reward in &rewards { + match reward.rtype { + RewardType::Leader => { + spo_rewards.operator_rewards += reward.amount; + } + RewardType::Member => { + num_delegators_paid += 1; + total_paid_to_delegators += reward.amount; + } + } + spo_rewards.total_rewards += reward.amount; + total_paid_to_pools += reward.amount; + result.total_paid += reward.amount; + } - Self::calculate_spo_rewards( - operator_id, - spo, - blocks_produced as u64, - total_blocks as u64, - &stake_rewards, - &total_supply, - &relative_pool_saturation_size, - &pledge_influence_factor, - params, - self.go.clone(), - &mut result, - &mut total_paid_to_pools, - &mut total_paid_to_delegators, - &mut num_delegators_paid, - ); + result.rewards.insert(operator_id.clone(), rewards); + result.spo_rewards.push((operator_id.clone(), spo_rewards)); } + } - info!( - num_delegators_paid, - total_paid_to_delegators, - total_paid_to_pools, - total = result.total_paid, - "Rewards actually paid:" - ); + info!( + num_delegators_paid, + total_paid_to_delegators, + num_pools_paid, + total_paid_to_pools, + total = result.total_paid, + "Rewards actually paid:" + ); - Ok(result) - } + Ok(result) +} - fn calculate_spo_rewards( - operator_id: &KeyHash, - spo: &SnapshotSPO, - blocks_produced: u64, - total_blocks: u64, - stake_rewards: &BigDecimal, - total_supply: &BigDecimal, - relative_pool_saturation_size: &BigDecimal, - pledge_influence_factor: &BigDecimal, - params: &ShelleyParams, - snapshot: Arc, - result: &mut RewardsResult, - total_paid_to_pools: &mut Lovelace, - total_paid_to_delegators: &mut Lovelace, - num_delegators_paid: &mut usize, - ) { - // Actual blocks produced as proportion of epoch (Beta) - let relative_blocks = BigDecimal::from(blocks_produced) / BigDecimal::from(total_blocks); - - // Active stake (sigma) - let pool_stake = BigDecimal::from(spo.total_stake); - if pool_stake.is_zero() { - // No stake, no rewards or earnings - return; - } +/// Calculate rewards for an individual SPO +fn calculate_spo_rewards( + operator_id: &KeyHash, + spo: &SnapshotSPO, + blocks_produced: u64, + total_blocks: usize, + stake_rewards: &BigDecimal, + total_supply: &BigDecimal, + relative_pool_saturation_size: &BigDecimal, + pledge_influence_factor: &BigDecimal, + params: &ShelleyParams, + staking: Arc, + staking_reward_account_is_registered: bool, +) -> Vec { + // Actual blocks produced as proportion of epoch (Beta) + let relative_blocks = BigDecimal::from(blocks_produced) / BigDecimal::from(total_blocks as u64); + + // Active stake (sigma) + let pool_stake = BigDecimal::from(spo.total_stake); + if pool_stake.is_zero() { + // No stake, no rewards or earnings + return vec![]; + } - // Get the stake actually delegated by the owners accounts to this SPO - let pool_owner_stake = - snapshot.get_stake_delegated_to_spo_by_addresses(&operator_id, &spo.pool_owners); - - // If they haven't met their pledge, no dice - if pool_owner_stake < spo.pledge { - warn!( - "SPO {} has owner stake {} less than pledge {} - skipping", - hex::encode(&operator_id), - pool_owner_stake, - spo.pledge - ); - return; - } + // Get the stake actually delegated by the owners accounts to this SPO + let pool_owner_stake = + staking.get_stake_delegated_to_spo_by_addresses(&operator_id, &spo.pool_owners); + + // If they haven't met their pledge, no dice + if pool_owner_stake < spo.pledge { + debug!( + "SPO {} has owner stake {} less than pledge {} - skipping", + hex::encode(&operator_id), + pool_owner_stake, + spo.pledge + ); + return vec![]; + } - let pool_pledge = BigDecimal::from(&spo.pledge); - - // Relative stake as fraction of total supply (sigma), and capped with 1/k (sigma') - let relative_pool_stake = &pool_stake / total_supply; - let capped_relative_pool_stake = min(&relative_pool_stake, &relative_pool_saturation_size); - - // Stake pledged by operator (s) and capped with 1/k (s') - let relative_pool_pledge = &pool_pledge / total_supply; - let capped_relative_pool_pledge = - min(&relative_pool_pledge, &relative_pool_saturation_size); - - // Get the optimum reward for this pool - let optimum_rewards = ((stake_rewards / (BigDecimal::one() + pledge_influence_factor)) - * (capped_relative_pool_stake - + (capped_relative_pool_pledge - * pledge_influence_factor - * (capped_relative_pool_stake - - (capped_relative_pool_pledge - * ((relative_pool_saturation_size - capped_relative_pool_stake) - / relative_pool_saturation_size)))) - / relative_pool_saturation_size)) + let pool_pledge = BigDecimal::from(&spo.pledge); + + // Relative stake as fraction of total supply (sigma), and capped with 1/k (sigma') + let relative_pool_stake = &pool_stake / total_supply; + let capped_relative_pool_stake = min(&relative_pool_stake, &relative_pool_saturation_size); + + // Stake pledged by operator (s) and capped with 1/k (s') + let relative_pool_pledge = &pool_pledge / total_supply; + let capped_relative_pool_pledge = min(&relative_pool_pledge, &relative_pool_saturation_size); + + // Get the optimum reward for this pool + let optimum_rewards = ((stake_rewards / (BigDecimal::one() + pledge_influence_factor)) + * (capped_relative_pool_stake + + (capped_relative_pool_pledge + * pledge_influence_factor + * (capped_relative_pool_stake + - (capped_relative_pool_pledge + * ((relative_pool_saturation_size - capped_relative_pool_stake) + / relative_pool_saturation_size)))) + / relative_pool_saturation_size)) + .with_scale(0); + + // If decentralisation_param >= 0.8 => performance = 1 + // Shelley Delegation Spec 3.8.3 + let decentralisation = ¶ms.protocol_params.decentralisation_param; + let pool_performance = if decentralisation >= &RationalNumber::new(8, 10) { + BigDecimal::one() + } else { + relative_blocks.clone() / relative_pool_stake.clone() + }; + + // Get actual pool rewards + let pool_rewards = (&optimum_rewards * &pool_performance).with_scale(0); + + debug!(blocks=blocks_produced, %pool_stake, %relative_pool_stake, %relative_blocks, + %pool_performance, %optimum_rewards, %pool_rewards, pool_owner_stake, %pool_pledge, + "Pool {}", hex::encode(&operator_id)); + + // Subtract fixed costs + let fixed_cost = BigDecimal::from(spo.fixed_cost); + let mut rewards = Vec::::new(); + let spo_benefit = if pool_rewards <= fixed_cost { + debug!("Rewards < cost - all paid to SPO"); + + // No margin or pledge reward if under cost - all goes to SPO + pool_rewards.to_u64().unwrap_or(0) + } else { + // Enough left over for some margin split + let margin = + BigDecimal::from(spo.margin.numerator) / BigDecimal::from(spo.margin.denominator); + + let relative_owner_stake = &pool_owner_stake / total_supply; + let margin_cost = ((&pool_rewards - &fixed_cost) + * (&margin + + (BigDecimal::one() - &margin) * (relative_owner_stake / relative_pool_stake))) .with_scale(0); + let costs = &fixed_cost + &margin_cost; + + // Pay the delegators - split remainder in proportional to delegated stake, + // * as it was 2 epochs ago * + + // You'd think this was just pool_rewards-costs here, but the Haskell code recalculates + // the margin without the relative_owner_stake term !? + let to_delegators = (&pool_rewards - &fixed_cost) * (BigDecimal::one() - &margin); // Note keep frac part + let mut total_paid: u64 = 0; + let mut delegators_paid: usize = 0; + if !to_delegators.is_zero() { + let total_stake = BigDecimal::from(spo.total_stake); + for (hash, stake) in &spo.delegators { + let proportion = BigDecimal::from(stake) / &total_stake; + + // and hence how much of the total reward they get + let reward = &to_delegators * &proportion; + let to_pay = reward.with_scale(0).to_u64().unwrap_or(0); + + debug!("Reward stake {stake} -> proportion {proportion} of SPO rewards {to_delegators} -> {to_pay} to hash {}", + hex::encode(hash)); + + // Pool owners don't get member rewards (seems unfair!) + if spo.pool_owners.contains(hash) { + debug!( + "Skipping pool owner reward account {}, losing {to_pay}", + hex::encode(hash) + ); + continue; + } - // If decentralisation_param >= 0.8 => performance = 1 - // Shelley Delegation Spec 3.8.3 - let decentralisation = ¶ms.protocol_params.decentralisation_param; - let pool_performance = if decentralisation >= &RationalNumber::new(8, 10) { - BigDecimal::one() - } else { - relative_blocks.clone() / relative_pool_stake.clone() - }; - - // Get actual pool rewards - let pool_rewards = (&optimum_rewards * &pool_performance).with_scale(0); - - info!(blocks=blocks_produced, %pool_stake, %relative_pool_stake, %relative_blocks, - %pool_performance, %optimum_rewards, %pool_rewards, - "Pool {}", hex::encode(operator_id.clone())); - - // Subtract fixed costs - let fixed_cost = BigDecimal::from(spo.fixed_cost); - let spo_benefit = if pool_rewards <= fixed_cost { - info!("Rewards < cost - all paid to SPO"); - - // No margin or pledge reward if under cost - all goes to SPO - pool_rewards.to_u64().unwrap_or(0) - } else { - // Enough left over for some margin split - let margin = ((&pool_rewards - &fixed_cost) - * BigDecimal::from(spo.margin.numerator) // TODO use RationalNumber - / BigDecimal::from(spo.margin.denominator)) - .with_scale(0); - let costs = &fixed_cost + &margin; - let remainder = &pool_rewards - &costs; - - // Pay the delegators - split remainder in proportional to delegated stake, - // * as it was 2 epochs ago * - let to_delegators = remainder.to_u64().unwrap_or(0); - if to_delegators > 0 { - let total_stake = BigDecimal::from(spo.total_stake); - for (hash, stake) in &spo.delegators { - let proportion = BigDecimal::from(stake) / &total_stake; - - // and hence how much of the total reward they get - let reward = BigDecimal::from(to_delegators) * &proportion; - let to_pay = reward.with_scale(0).to_u64().unwrap_or(0); - - debug!("Reward stake {stake} -> proportion {proportion} of SPO rewards {to_delegators} -> {to_pay} to hash {}", - hex::encode(&hash)); - - // Transfer from reserves to this account - result.rewards.push((hash.clone(), to_pay)); - result.total_paid += to_pay; - - *num_delegators_paid += 1; - *total_paid_to_delegators += to_pay; + // Check pool's reward address - removing e1 prefix + // TODO use StakeAddress.get_hash() + if spo.reward_account[1..] == *hash { + debug!( + "Skipping pool reward account {}, losing {to_pay}", + hex::encode(hash) + ); + continue; } + + // TODO Shelley-until-Allegra bug if same reward account used for multiple + // SPOs - check pool's reward address but only before Allegra? + + // Transfer from reserves to this account + rewards.push(RewardDetail { + account: hash.clone(), + rtype: RewardType::Member, + amount: to_pay, + }); + total_paid += to_pay; + delegators_paid += 1; } + } - info!(%fixed_cost, %margin, to_delegators, "Reward split:"); - - costs.to_u64().unwrap_or(0) - }; - result.rewards.push((spo.reward_account.clone(), spo_benefit)); - result.spo_rewards.push(( - operator_id.clone(), - SPORewards { - total_rewards: pool_rewards.to_u64().unwrap_or(0), - operator_rewards: spo_benefit, - }, - )); - result.total_paid += spo_benefit; - *total_paid_to_pools += spo_benefit; + debug!(%fixed_cost, %margin_cost, leader_reward=%costs, %to_delegators, total_paid, + delegators_paid, "Reward split:"); + + costs.to_u64().unwrap_or(0) + }; + + // SPO's reward account from staking time must be registered now + // TODO horrors about the time of registration/deregistration - depends on whether + // it was deregistered before 4 * k blocks (actually 4 * k * 20 slots) into the epoch! + // For now, "now" = time of last ('performance') snapshot + if staking_reward_account_is_registered { + rewards.push(RewardDetail { + // TODO Hack to remove e1 header - needs resolving properly with StakeAddress + account: RewardAccount::from(&spo.reward_account[1..]), + rtype: RewardType::Leader, + amount: spo_benefit, + }); + } else { + info!( + "SPO {}'s reward account {} isn't registered - dropping their reward of {}", + hex::encode(&operator_id), + hex::encode(&spo.reward_account), + spo_benefit, + ); } + + rewards } diff --git a/modules/accounts_state/src/snapshot.rs b/modules/accounts_state/src/snapshot.rs index ba456ebe..f3c394fc 100644 --- a/modules/accounts_state/src/snapshot.rs +++ b/modules/accounts_state/src/snapshot.rs @@ -1,10 +1,11 @@ //! Acropolis AccountsState: snapshot for rewards calculations use crate::state::{Pots, StakeAddressState}; -use acropolis_common::{KeyHash, Lovelace, PoolRegistration, Ratio, RewardAccount}; +use acropolis_common::{KeyHash, Lovelace, PoolRegistration, Ratio, RewardAccount, StakeAddress}; use imbl::OrdMap; use std::collections::HashMap; -use tracing::info; +use std::sync::Arc; +use tracing::{error, info}; /// SPO data for stake snapshot #[derive(Debug, Default)] @@ -30,6 +31,9 @@ pub struct SnapshotSPO { /// Reward account pub reward_account: RewardAccount, + /// Is the reward account from two epochs ago registered at the time of this snapshot? + pub two_previous_reward_account_is_registered: bool, + /// Pool owners pub pool_owners: Vec, } @@ -46,8 +50,8 @@ pub struct Snapshot { /// Persistent pot values pub pots: Pots, - /// Fees - pub fees: Lovelace, + /// Total blocks + pub blocks: usize, } impl Snapshot { @@ -58,12 +62,13 @@ impl Snapshot { spos: &OrdMap, spo_block_counts: &HashMap, pots: &Pots, - fees: Lovelace, + blocks: usize, + two_previous_snapshot: Arc, ) -> Self { let mut snapshot = Self { _epoch: epoch, pots: pots.clone(), - fees, + blocks, ..Self::default() }; @@ -71,12 +76,13 @@ impl Snapshot { // Note this is _active_ stake, for reward calculations, and hence doesn't include rewards let mut total_stake: Lovelace = 0; for (hash, sas) in stake_addresses { - if sas.utxo_value > 0 { + let active_stake = sas.utxo_value + sas.rewards; + if sas.registered && active_stake > 0 { if let Some(spo_id) = &sas.delegated_spo { // Only clone if insertion is needed if let Some(snap_spo) = snapshot.spos.get_mut(spo_id) { - snap_spo.delegators.push((hash.clone(), sas.utxo_value)); - snap_spo.total_stake += sas.utxo_value; + snap_spo.delegators.push((hash.clone(), active_stake)); + snap_spo.total_stake += active_stake; } else { // Find in the SPO list let Some(spo) = spos.get(spo_id) else { @@ -86,22 +92,51 @@ impl Snapshot { // See how many blocks produced let blocks_produced = spo_block_counts.get(spo_id).copied().unwrap_or(0); + + // Check if the reward account from two epochs ago is still registered + // TODO should spo.reward_account be a StakeAddress to begin with? + let two_previous_reward_account_is_registered = + match two_previous_snapshot.spos.get(spo_id) { + Some(old_spo) => { + match StakeAddress::from_binary(&old_spo.reward_account) { + Ok(spo_reward_address) => { + let spo_reward_hash = spo_reward_address.get_hash(); + stake_addresses + .get(spo_reward_hash) + .map(|sas| sas.registered) + .unwrap_or(false) + } + Err(e) => { + error!( + "Can't decode reward address for SPO {}: {e}", + hex::encode(&spo_id) + ); + + false + } + } + } + None => false, + }; + + // Add the new one snapshot.spos.insert( spo_id.clone(), SnapshotSPO { - delegators: vec![(hash.clone(), sas.utxo_value)], - total_stake: sas.utxo_value, + delegators: vec![(hash.clone(), active_stake)], + total_stake: active_stake, pledge: spo.pledge, fixed_cost: spo.cost, margin: spo.margin.clone(), blocks_produced, pool_owners: spo.pool_owners.clone(), reward_account: spo.reward_account.clone(), + two_previous_reward_account_is_registered, }, ); } } - total_stake += sas.utxo_value; + total_stake += active_stake; } } @@ -117,7 +152,7 @@ impl Snapshot { deposits = pots.deposits, total_stake, spos = snapshot.spos.len(), - fees, + blocks, "Snapshot" ); @@ -154,7 +189,7 @@ mod tests { use super::*; #[test] - fn get_stake_snapshot_counts_stake_and_ignores_undelegated_and_zero_values() { + fn get_stake_snapshot_counts_stake_and_ignores_unregistered_undelegated_and_zero_values() { let spo1: KeyHash = vec![0x01]; let spo2: KeyHash = vec![0x02]; @@ -162,12 +197,14 @@ mod tests { let addr2: KeyHash = vec![0x12]; let addr3: KeyHash = vec![0x13]; let addr4: KeyHash = vec![0x14]; + let addr5: KeyHash = vec![0x15]; let mut stake_addresses: HashMap = HashMap::new(); stake_addresses.insert( addr1.clone(), StakeAddressState { utxo_value: 42, + registered: true, delegated_spo: Some(spo1.clone()), ..StakeAddressState::default() }, @@ -176,6 +213,7 @@ mod tests { addr2.clone(), StakeAddressState { utxo_value: 99, + registered: true, delegated_spo: Some(spo2.clone()), ..StakeAddressState::default() }, @@ -184,6 +222,7 @@ mod tests { addr3.clone(), StakeAddressState { utxo_value: 0, + registered: true, delegated_spo: Some(spo1.clone()), ..StakeAddressState::default() }, @@ -192,6 +231,16 @@ mod tests { addr4.clone(), StakeAddressState { utxo_value: 1000000, + registered: true, + delegated_spo: None, + ..StakeAddressState::default() + }, + ); + stake_addresses.insert( + addr5.clone(), + StakeAddressState { + utxo_value: 2000000, + registered: false, delegated_spo: None, ..StakeAddressState::default() }, @@ -208,6 +257,7 @@ mod tests { &spo_block_counts, &Pots::default(), 0, + Arc::new(Snapshot::default()), ); assert_eq!(snapshot.spos.len(), 2); diff --git a/modules/accounts_state/src/state.rs b/modules/accounts_state/src/state.rs index 00179be6..67e2b5b7 100644 --- a/modules/accounts_state/src/state.rs +++ b/modules/accounts_state/src/state.rs @@ -1,7 +1,8 @@ //! Acropolis AccountsState: State storage use crate::monetary::calculate_monetary_change; -use crate::rewards::{RewardsResult, RewardsState}; +use crate::rewards::{calculate_rewards, RewardsResult}; use crate::snapshot::Snapshot; +use crate::verifier::Verifier; use acropolis_common::protocol_params::ProtocolParams; use acropolis_common::SPORewards; use acropolis_common::{ @@ -56,7 +57,7 @@ pub struct DRepDelegationDistribution { } /// Global 'pot' account state -#[derive(Debug, Default, Clone, serde::Serialize)] +#[derive(Debug, Default, PartialEq, Clone, serde::Serialize)] pub struct Pots { /// Unallocated reserves pub reserves: Lovelace, @@ -68,6 +69,28 @@ pub struct Pots { pub deposits: Lovelace, } +/// State for rewards calculation +#[derive(Debug, Default, Clone)] +pub struct EpochSnapshots { + /// Latest snapshot (epoch i) + pub mark: Arc, + + /// Previous snapshot (epoch i-1) + pub set: Arc, + + /// One before that (epoch i-2) + pub go: Arc, +} + +impl EpochSnapshots { + /// Push a new snapshot + pub fn push(&mut self, latest: Snapshot) { + self.go = self.set.clone(); + self.set = self.mark.clone(); + self.mark = Arc::new(latest); + } +} + /// Overall state - stored per block #[derive(Debug, Default, Clone)] pub struct State { @@ -78,8 +101,8 @@ pub struct State { /// Wrapped in an Arc so it doesn't get cloned in full by StateHistory stake_addresses: Arc>>, - /// Reward state - short history of snapshots - rewards_state: RewardsState, + /// Short history of snapshots + epoch_snapshots: EpochSnapshots, /// Global account pots pots: Pots, @@ -199,12 +222,14 @@ impl State { /// epoch: Number of epoch we are entering /// total_fees: Total fees taken in previous epoch /// spo_block_counts: Count of blocks minted by operator ID in previous epoch + /// verifier: Verifier against Haskell node output // Follows the general scheme in https://docs.cardano.org/about-cardano/learn/pledging-rewards fn enter_epoch( &mut self, epoch: u64, total_fees: u64, spo_block_counts: HashMap, + verifier: &Verifier, ) -> Result<()> { // TODO HACK! Investigate why this differs to our calculated reserves after AVVM // 13,887,515,255 - as we enter 208 (Shelley) @@ -243,21 +268,21 @@ impl State { let total_non_obft_blocks = total_blocks - obft_block_count; info!(total_blocks, total_non_obft_blocks, "Block counts:"); - // Update the reserves and treasury (monetary.rs) - // TODO note using last-but-one epoch's fees for reward pot - why? - let monetary_change = calculate_monetary_change( - &shelley_params, - &self.pots, - self.rewards_state.mark.fees, - total_non_obft_blocks, - )?; - self.pots = monetary_change.pots; + info!( + epoch, + reserves = self.pots.reserves, + treasury = self.pots.treasury, + "Entering" + ); // Pay the refunds and MIRs self.pay_pool_refunds(); self.pay_stake_refunds(); self.pay_mirs(); + // Verify pots state + verifier.verify_pots(epoch, &self.pots); + // Capture a new snapshot and push it to state let snapshot = Snapshot::new( epoch, @@ -265,22 +290,37 @@ impl State { &self.spos, &spo_block_counts, &self.pots, - total_fees, + total_blocks, + // Pass in two-previous epoch snapshot for capture of SPO reward accounts + self.epoch_snapshots.set.clone(), // Will become 'go' in the next line! ); - self.rewards_state.push(snapshot); + self.epoch_snapshots.push(snapshot); - // Stop here if no blocks to pay out on - if total_blocks == 0 { - return Ok(()); - } + // Update the reserves and treasury (monetary.rs) + let monetary_change = calculate_monetary_change( + &shelley_params, + &self.pots, + total_fees, + total_non_obft_blocks, + )?; + self.pots = monetary_change.pots; + + info!( + epoch, + reserves = self.pots.reserves, + treasury = self.pots.treasury, + "After monetary change" + ); - let rs = self.rewards_state.clone(); + let performance = self.epoch_snapshots.mark.clone(); + let staking = self.epoch_snapshots.go.clone(); self.epoch_rewards_task = Arc::new(Mutex::new(Some(spawn_blocking(move || { // Calculate reward payouts - rs.calculate_rewards( + calculate_rewards( epoch, + performance, + staking, &shelley_params, - total_blocks, monetary_change.stake_rewards, ) })))); @@ -543,12 +583,13 @@ impl State { /// Handle an EpochActivityMessage giving total fees and block counts by VRF key for /// the just-ended epoch - /// This also returns SPO rewards for publishing to the SPDD topic (For epoch N) pub async fn handle_epoch_activity( &mut self, ea_msg: &EpochActivityMessage, + verifier: &Verifier, ) -> Result> { let mut spo_rewards: Vec<(KeyHash, SPORewards)> = Vec::new(); + // Reverse map of VRF key to SPO operator ID let vrf_to_operator: HashMap = self.spos.iter().map(|(id, spo)| (spo.vrf_key_hash.clone(), id.clone())).collect(); @@ -572,13 +613,19 @@ impl State { } } }; + // If rewards have been calculated, save the results if let Some(task) = task.take() { match task.await { Ok(Ok(reward_result)) => { + // Verify them + verifier.verify_rewards(reward_result.epoch, &reward_result); + // Pay the rewards - for (account, amount) in reward_result.rewards { - self.add_to_reward(&account, amount); + for (_, rewards) in reward_result.rewards { + for reward in rewards { + self.add_to_reward(&reward.account, reward.amount); + } } // save SPO rewards @@ -591,8 +638,12 @@ impl State { } }; // Enter epoch - note the message specifies the epoch that has just *ended* - self.enter_epoch(ea_msg.epoch + 1, ea_msg.total_fees, spo_block_counts)?; - + self.enter_epoch( + ea_msg.epoch + 1, + ea_msg.total_fees, + spo_block_counts, + verifier, + )?; Ok(spo_rewards) } @@ -614,6 +665,38 @@ impl State { // Check for how many new SPOs let new_count = new_spos.keys().filter(|id| !self.spos.contains_key(*id)).count(); + // Log new ones and pledge/cost/margin changes + for (id, spo) in new_spos.iter() { + match self.spos.get(id) { + Some(old_spo) => { + if spo.pledge != old_spo.pledge + || spo.cost != old_spo.cost + || spo.margin != old_spo.margin + { + debug!( + epoch = spo_msg.epoch, + pledge = spo.pledge, + cost = spo.cost, + margin = ?spo.margin, + "Updated parameters for SPO {}", + hex::encode(id) + ); + } + } + + _ => { + debug!( + epoch = spo_msg.epoch, + pledge = spo.pledge, + cost = spo.cost, + margin = ?spo.margin, + "Registered new SPO {}", + hex::encode(id) + ); + } + } + } + // They've each paid their deposit, so increment that (the UTXO spend is taken // care of in UTXOState) let total_deposits = (new_count as u64) * deposit; diff --git a/modules/accounts_state/src/verifier.rs b/modules/accounts_state/src/verifier.rs new file mode 100644 index 00000000..a5d59530 --- /dev/null +++ b/modules/accounts_state/src/verifier.rs @@ -0,0 +1,269 @@ +//! Verification of calculated values against captured CSV from Haskell node / DBSync +use crate::rewards::{RewardDetail, RewardType, RewardsResult}; +use crate::state::Pots; +use acropolis_common::{KeyHash, RewardAccount}; +use hex::FromHex; +use itertools::EitherOrBoth::{Both, Left, Right}; +use itertools::Itertools; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use tracing::{debug, error, info, warn}; + +/// Verifier +pub struct Verifier { + /// Map of pots values for every epoch + epoch_pots: BTreeMap, + + /// Template (with {} for epoch) for rewards files + rewards_file_template: Option, +} + +impl Verifier { + /// Construct empty + pub fn new() -> Self { + Self { + epoch_pots: BTreeMap::new(), + rewards_file_template: None, + } + } + + /// Read in a pots file + pub fn read_pots(&mut self, path: &str) { + let mut reader = match csv::Reader::from_path(path) { + Ok(reader) => reader, + Err(err) => { + error!("Failed to load pots CSV from {path}: {err} - not verifying"); + return; + } + }; + + // Expect CSV header: epoch,reserves,treasury,deposits + for result in reader.deserialize() { + let (epoch, reserves, treasury, deposits): (u64, u64, u64, u64) = match result { + Ok(row) => row, + Err(err) => { + error!("Bad row in {path}: {err} - skipping"); + continue; + } + }; + + self.epoch_pots.insert( + epoch, + Pots { + reserves, + treasury, + deposits, + }, + ); + } + } + + /// Read in rewards files + // Actually just stores the template and reads them on demand + pub fn read_rewards(&mut self, path: &str) { + self.rewards_file_template = Some(path.to_string()); + } + + /// Verify an epoch, logging any errors + pub fn verify_pots(&self, epoch: u64, pots: &Pots) { + if self.epoch_pots.is_empty() { + return; + } + + if let Some(desired_pots) = self.epoch_pots.get(&epoch) { + if pots.reserves != desired_pots.reserves { + error!( + epoch = epoch, + calculated = pots.reserves, + desired = desired_pots.reserves, + difference = desired_pots.reserves as i64 - pots.reserves as i64, + "Verification mismatch: reserves for" + ); + } + + if pots.treasury != desired_pots.treasury { + error!( + epoch = epoch, + calculated = pots.treasury, + desired = desired_pots.treasury, + difference = desired_pots.treasury as i64 - pots.treasury as i64, + "Verification mismatch: treasury for" + ); + } + + if pots.deposits != desired_pots.deposits { + error!( + epoch = epoch, + calculated = pots.deposits, + desired = desired_pots.deposits, + difference = desired_pots.deposits as i64 - pots.deposits as i64, + "Verification mismatch: deposits for" + ); + } + + if pots == desired_pots { + info!(epoch = epoch, "Verification success for"); + } + } else { + warn!("Epoch {epoch} not represented in verify test data"); + } + } + + /// Sort rewards for zipper compare - type first, then by account + fn sort_rewards(left: &RewardDetail, right: &RewardDetail) -> Ordering { + match (&left.rtype, &right.rtype) { + (RewardType::Leader, RewardType::Member) => Ordering::Less, + (RewardType::Member, RewardType::Leader) => Ordering::Greater, + _ => left.account.cmp(&right.account), + } + } + + /// Verify rewards, logging any errors + pub fn verify_rewards(&self, epoch: u64, rewards: &RewardsResult) { + if let Some(template) = &self.rewards_file_template { + let path = template.replace("{}", &epoch.to_string()); + + // Silently return if there's no file for it + let mut reader = match csv::Reader::from_path(&path) { + Ok(reader) => reader, + _ => return, + }; + + // Expect CSV header: spo,address,type,amount + let mut expected_rewards: BTreeMap> = BTreeMap::new(); + for result in reader.deserialize() { + let (spo, address, rtype, amount): (String, String, String, u64) = match result { + Ok(row) => row, + Err(err) => { + error!("Bad row in {path}: {err} - skipping"); + continue; + } + }; + + let Ok(spo) = Vec::from_hex(&spo) else { + error!("Bad hex in {path} for SPO: {spo} - skipping"); + continue; + }; + let Ok(account) = Vec::from_hex(&address) else { + error!("Bad hex in {path} for address: {address} - skipping"); + continue; + }; + + // Ignore 0 amounts + if amount == 0 { + continue; + } + + // Convert from string and ignore refunds + let rtype = match rtype.as_str() { + "leader" => RewardType::Leader, + "member" => RewardType::Member, + _ => continue, + }; + + // Convert account with e1 header to just hash + // TODO: use StakeAddress, skipping first byte (e1) for now + let account = RewardAccount::from(&account[1..]); + + expected_rewards.entry(spo).or_default().push(RewardDetail { + account, + rtype, + amount, + }); + } + + info!( + epoch, + "Read rewards verification data for {} SPOs", + expected_rewards.len() + ); + + // TODO compare rewards with expected_rewards, log missing members/leaders in both + // directions, changes of value + let mut errors: usize = 0; + for either in expected_rewards + .into_iter() + .merge_join_by(rewards.rewards.clone().into_iter(), |i, j| i.0.cmp(&j.0)) + { + match either { + Left(expected_spo) => { + error!( + "Missing rewards SPO: {} {} rewards", + hex::encode(&expected_spo.0), + expected_spo.1.len() + ); + errors += 1; + } + Right(actual_spo) => { + error!( + "Extra rewards SPO: {} {} rewards", + hex::encode(&actual_spo.0), + actual_spo.1.len() + ); + errors += 1; + } + Both(mut expected_spo, mut actual_spo) => { + expected_spo.1.sort_by(Self::sort_rewards); + actual_spo.1.sort_by(Self::sort_rewards); + for either in expected_spo + .1 + .into_iter() + .merge_join_by(actual_spo.1.into_iter(), |i, j| { + i.account.cmp(&j.account.clone()) + }) + { + match either { + Left(expected) => { + error!( + "Missing reward: SPO {} account {} {:?} {}", + hex::encode(&expected_spo.0), + hex::encode(&expected.account), + expected.rtype, + expected.amount + ); + errors += 1; + } + Right(actual) => { + error!( + "Extra reward: SPO {} account {} {:?} {}", + hex::encode(&actual_spo.0), + hex::encode(&actual.account), + actual.rtype, + actual.amount + ); + errors += 1; + } + Both(expected, actual) => { + if expected.amount != actual.amount { + error!("Different reward: SPO {} account {} {:?} expected {}, actual {} ({})", + hex::encode(&expected_spo.0), + hex::encode(&expected.account), + expected.rtype, + expected.amount, + actual.amount, + actual.amount as i64-expected.amount as i64); + errors += 1; + } else { + debug!( + "Reward match: SPO {} account {} {:?} {}", + hex::encode(&expected_spo.0), + hex::encode(&expected.account), + expected.rtype, + expected.amount + ); + } + } + } + } + } + } + } + + if errors == 0 { + info!(epoch, "Rewards verification OK"); + } else { + error!(errors, epoch, "Rewards verification:"); + } + } + } +} diff --git a/modules/accounts_state/test-data/pots.mainnet.csv b/modules/accounts_state/test-data/pots.mainnet.csv new file mode 100644 index 00000000..52c797dc --- /dev/null +++ b/modules/accounts_state/test-data/pots.mainnet.csv @@ -0,0 +1,372 @@ +epoch,reserves,treasury,deposits +209,13286160713028443,8332813711755,441012000000 +210,13278197552770393,16306644182013,533870000000 +211,13270236767315870,24275595982960,594636000000 +212,13262280841681299,32239292149804,626252000000 +213,13247093198353459,40198464232058,651738000000 +214,13230232787944838,48148335794725,674438000000 +215,13212986170770203,55876297807656,695040000000 +216,13195031638588164,63707722011028,706174000000 +217,13176528835451373,71629614335572,719058000000 +218,13157936081322000,79429791062499,729244000000 +219,13139088245733216,87229346231023,735038000000 +220,13120582265809833,94812346026398,739774000000 +221,13101550250680254,102526032953120,748540000000 +222,13082116350342059,110388072013679,755528000000 +223,13062655956639744,118208699416236,762498000000 +224,13042967905529920,125940338514332,766164000000 +225,13023198387000242,133613085475447,769884000000 +226,13003168356561692,141320961489871,771722000000 +227,12983158507901822,148966611367094,784010000000 +228,12963125292915959,156672155969216,804642000000 +229,12943288527645190,164146740314244,831368000000 +230,12923287136043547,171650774380874,845220000000 +231,12901374614727880,179282860001401,857626000000 +232,12879420804989068,186903589285290,875348000000 +233,12857569396931762,194427116830134,904328000000 +234,12835708801543869,201938554280313,927916000000 +235,12813555587125201,209567887755185,955736000000 +236,13112607631777589,217021605777163,981170000000 +237,13092064399031575,224755310169411,980456000000 +238,13070468429326735,232303490719413,995120000000 +239,13047906930168382,240110310231353,1008602000000 +240,13025619519514576,247808783829569,1027018000000 +241,13001889545532991,255497935879168,1063042000000 +242,12977832637176786,261254564022929,1088732000000 +243,12953750797382735,268861912841117,1121866000000 +244,12929566121367277,276588875846127,1153290000000 +245,12905245994461083,284352137586764,1176314000000 +246,12880948865137767,292077855298344,1226242000000 +247,12856514256368052,299811858331381,1273350000000 +248,12832265659375334,307469331873040,1345858000000 +249,12807979499513648,315149768885434,1433774000000 +250,12783925491495810,322747474373585,1531256000000 +251,12759915292888389,330358411585833,1618620000000 +252,12736017185934876,337791818166627,1719888000000 +253,12711785646200955,345342263589600,1805112000000 +254,12688276934220030,352786796113777,1878940000000 +255,12665043040299013,360161954760303,1943702000000 +256,12641992243076681,367449804338663,1991750000000 +257,12618536190581649,374930989230241,2047106000000 +258,12595569991053045,382322104327451,2090390000000 +259,12572853459591508,389627105010745,2128532000000 +260,12549908795306837,396980740382998,2171730000000 +261,12527064254027631,404243604632007,2205152000000 +262,12503994895122529,411581611048417,2242608000000 +263,12480979868875769,418916130935065,2284322000000 +264,12457884526305649,426275062812584,2321018000000 +265,12434957914838905,433592879391282,2370466000000 +266,12411854285907938,440912464520623,2410892000000 +267,12388414463130348,448318487744510,2471828000000 +268,12365028524445435,455707939470671,2520964000000 +269,12341518728137774,463139474652805,2570670000000 +270,12318321891398929,470491973129948,2621948000000 +271,12295113815583721,477865672126854,2661156000000 +272,12270780512858536,485181573455503,2692250000000 +273,12247689462766311,492487924250431,2733218000000 +274,12224475923697967,499813627216983,2761062000000 +275,12201340389910655,507109701568970,2783448000000 +276,12178189995478896,514411351908730,2817344000000 +277,12155122184954568,521694330279703,2844760000000 +278,12132264814197796,528893184223406,2866194000000 +279,12109284816700841,530611692658077,2891206000000 +280,12086332559356592,537801015045532,2909030000000 +281,12063362662818908,544993458360858,2934402000000 +282,12040662411646400,552109061206815,2957578000000 +283,12017762412942030,559301273768095,2986618000000 +284,11994919913555265,566496518637485,3012146000000 +285,11972182166198574,573642248264697,3041920000000 +286,11949345585782632,580813475958128,3080674000000 +287,11927003666950877,587866685636543,3107936000000 +288,11904389292477517,595010655786398,3140836000000 +289,11882381119547453,601999673290515,3174808000000 +290,11860319467481270,609002817835001,3212648000000 +291,11838146881718587,616008493107142,3246248000000 +292,11815842837779699,623039919820396,3278276000000 +293,11793690427914625,630037263793143,3312824000000 +294,11771504387608670,637024173474141,3338702000000 +295,11749349764763643,643991464810237,3362142000000 +296,11727557414227650,650852643461256,3378738000000 +297,11705653358398389,657738963676591,3404652000000 +298,11684019538729705,664536270685807,3410326000000 +299,11343887069673499,671319247451273,3428972000000 +300,11323054368200473,677910511293920,3446712000000 +301,11302106993169046,684275367847493,3461226000000 +302,11280717436923376,690958635875788,3483056000000 +303,11259657628761195,697557178525050,3511948000000 +304,11238469926202349,704231212591513,3537110000000 +305,11217568742767163,710796083937713,3565706000000 +306,11196633067737696,717435151480773,3593542000000 +307,11175759253425611,724010388331415,3613138000000 +308,11154816240369338,730601615685965,3634964000000 +309,11134121506196268,737110558232480,3646080000000 +310,11113347476573099,743638101982934,3665746000000 +311,11092893736994628,750091385613797,3680564000000 +312,11072246017826610,756609425944090,3691378000000 +313,11051631533654672,763121537857734,3707828000000 +314,11031385419250644,769551576595698,3735054000000 +315,11011700944995489,775800361280391,3759742000000 +316,10992003609815988,782079043389156,3841010000000 +317,10972584901107006,788297676230791,3893092000000 +318,10953227838823001,794490731721927,3915062000000 +319,10933650872361432,800775096758125,3930392000000 +320,10914331394835612,806985387511233,3946080000000 +321,10894905349913180,805605092526580,3961876000000 +322,10875612782714797,811794260532363,3979552000000 +323,10856555549633463,816782078123190,3986056000000 +324,10837286615744764,822987529535214,3994438000000 +325,10817894577514127,829237846580369,3998420000000 +326,10798756714539428,835407607356681,4004206000000 +327,10779457383188418,841610183661313,4006228000000 +328,10760324733104603,847806215028479,4018634000000 +329,10741397939952327,853941152115025,4030428000000 +330,10722297918395194,860166383169807,4035164000000 +331,10702736716126709,866276896064357,4037702000000 +332,10682871684482153,872471870892203,4039940000000 +333,10662954751304020,878653307632686,4045582000000 +334,10643350776202190,884736526609247,4050532000000 +335,10623895506123552,890778684245517,4057512000000 +336,10604486697782318,896790999922300,4060880000000 +337,10584926320452051,902856542906841,4063148000000 +338,10565374767957279,908925569281222,4055378000000 +339,10545586696000514,915064913598237,4057408000000 +340,10526055083287405,921148142062990,4057604000000 +341,10506491829002854,927231126863356,4057230000000 +342,10487149571006994,900379780082021,4056594000000 +343,10467739665223957,906447014546351,4053856000000 +344,10448049960041700,912584295685928,4058794000000 +345,10428470110767403,918713205275544,4063368000000 +346,10409105745481118,924761379220192,4067926000000 +347,10389506871116964,930867659133409,4074622000000 +348,10370377949854532,936834108750911,4072364000000 +349,10352411929782702,942432272103687,4076276000000 +350,10333899043124872,948203440354410,4080786000000 +351,10314641202700475,954249374293574,4083576000000 +352,10295428286983073,960277877068492,4082982000000 +353,10276330879384622,966281863995315,4087096000000 +354,10257589126504652,972182782671244,4090350000000 +355,10239288792981608,977973384876487,4091296000000 +356,10220679892460660,983837643868650,4095378000000 +357,10201748722980993,989776809984523,4098212000000 +358,10182918483065564,995690025485814,4102220000000 +359,10164188969499216,1001568454234009,4110912000000 +360,10145052424519948,1007570211324963,4114884000000 +361,10126038387141609,1013519055278692,4117024000000 +362,10106899741240898,1019513669653941,4118198000000 +363,10087736962737259,1025501175173226,4121012000000 +364,10068593369607165,1031482492741569,4120706000000 +365,10049419651037034,1037476762140256,4123762000000 +366,10030500598819482,1043351943571270,4125370000000 +367,10011568005046602,1049235663440299,4126586000000 +368,9992648700658977,1055149630369187,4134642000000 +369,9973897045344554,1060989482938007,4133918000000 +370,9954979169676611,1066870216358458,4130750000000 +371,9936114349742976,1072721304252673,4134376000000 +372,9917197168085496,1078611968858093,4136728000000 +373,9898528859081034,1084442000419800,4148936000000 +374,9879803953263264,1090284059434660,4153180000000 +375,9861116544907807,1056673748569830,4157814000000 +376,9842652064089820,1062423482265345,4163860000000 +377,9824321019586905,1068123742127903,4168372000000 +378,9806194893505457,1073761279310734,4171748000000 +379,9787739043117847,1079524909809257,4175764000000 +380,9769359542724410,1085266054287657,4176642000000 +381,9751099859203471,1090977794605066,4182808000000 +382,9732579730335267,1096769891327182,4180354000000 +383,9714372537673880,1102477860882235,4183238000000 +384,9696062028066324,1108231313584225,4190816000000 +385,9677898230050332,1113894699593937,4194146000000 +386,9659613006525152,1119569090557032,4197676000000 +387,9641213380131503,1125283441597315,4202968000000 +388,9623238621042507,1130915186538430,4214576000000 +389,9605003170159443,1136579323491238,4214182000000 +390,9586693644501587,1142264335933469,4218064000000 +391,9568737675719031,1147834888160445,4220870000000 +392,9550707326420076,1153424016337181,4218486000000 +393,9532573493585434,1159053622658953,4223998000000 +394,9514934935216517,1164535619945030,4227436000000 +395,9497133409472783,1170067020486250,4228366000000 +396,9479585689711008,1175513742012425,4231834000000 +397,9461815546187508,1181075985373286,4234062000000 +398,9443972566840349,1186758136337642,4236090000000 +399,9426301009576749,1192381615518303,4235794000000 +400,9408537858242252,1198058432944920,4236122000000 +401,9391113276049044,1203571262060792,4236772000000 +402,9373759520550477,1209099799354716,4232288000000 +403,9356641772388785,1214559938325861,4230422000000 +404,9339311033457902,1220072850270466,4234378000000 +405,9322254273087787,1225541030930729,4239488000000 +406,9305382922879304,1230997735876222,4242162000000 +407,9288701770557405,1236437634639208,4242294000000 +408,9272157715492362,1241944161023134,4247654000000 +409,9255427816188677,1247455873909450,4248382000000 +410,9238655017642055,1244154789634220,4250266000000 +411,9222302989055674,1249688145781837,4249888000000 +412,9206053137433248,1255205718727929,4251392000000 +413,9189561743422242,1261091761016337,4251704000000 +414,9173472878616637,1266965539860820,4269310000000 +415,9157220397160138,1272798631646023,4269412000000 +416,9141408669939478,1278342807280510,4265286000000 +417,9126046644944245,1283676763623901,4270772000000 +418,9110127202404423,1289082659079958,4272688000000 +419,9094338748570024,1294423169897744,4272966000000 +420,9078521499110565,1299778626218859,4271304000000 +421,9063032557660485,1305029315722194,4275542000000 +422,9047622199764891,1310263863997300,4278692000000 +423,9032034292857262,1315551035659310,4281878000000 +424,9016392244472160,1320864396635145,4283294000000 +425,9000866603128574,1326160702195945,4285226000000 +426,8985278126640115,1331451253889835,4287198000000 +427,8969722014329330,1336737020880096,4287416000000 +428,8954215876954027,1341996676328651,4296688000000 +429,8938829163644506,1347267485402985,4299530000000 +430,8923448699710227,1352522451375734,4299046000000 +431,8908035374574998,1357779071669941,4300254000000 +432,8892643599428594,1363023110216084,4302652000000 +433,8877370345904236,1368214253632528,4306320000000 +434,8862589414115358,1373239820320665,4302704000000 +435,8847342747476447,1378419161023568,4303460000000 +436,8832022058874836,1383599670487649,4304686000000 +437,8816766950673503,1388777091806873,4306844000000 +438,8801415188576255,1394003371321239,4308054000000 +439,8786093762361533,1399218565292671,4310106000000 +440,8770985210124101,1404363573077985,4310798000000 +441,8755712247133776,1409557056505625,4310814000000 +442,8740627073337488,1364721053361805,4297000000000 +443,8725270759466564,1369908624960600,4298926000000 +444,8710191636492473,1375019903319223,4301324000000 +445,8694963287980163,1380197545352859,4304792000000 +446,8679810673091783,1385334935731313,4307228000000 +447,8664777302912097,1390422658558876,4309924000000 +448,8649726897692243,1395516870297212,4310784000000 +449,8634608907096306,1400647065598924,4312398000000 +450,8619584135017586,1405748420583942,4310940000000 +451,8604731076564123,1410808637386707,4312718000000 +452,8589957179575095,1415822553230528,4314842000000 +453,8575057374648348,1420889749919979,4314824000000 +454,8560024968913102,1426000643140040,4318142000000 +455,8545305926326569,1431025090491411,4320424000000 +456,8530456126418253,1436093215624403,4326066000000 +457,8515623736425579,1441157257090799,4327234000000 +458,8501083191092514,1446160832567429,4327834000000 +459,8486301514701735,1451242730302673,4330556000000 +460,8471762024474669,1456239280414863,4332432000000 +461,8457302540309566,1461210460215224,4334490000000 +462,8442920926434661,1466149536674751,4338650000000 +463,8428636511768877,1471045157553368,4339376000000 +464,8414256054893475,1475975213267045,4341926000000 +465,8400262854415407,1480770938730322,4341992000000 +466,8386071088078952,1485636514586773,4343424000000 +467,8371794055621580,1490528575180145,4344300000000 +468,8357522743470265,1495424475832556,4345440000000 +469,8343083956124781,1500366891487428,4345570000000 +470,8328570503552617,1505337916378058,4343286000000 +471,8314145263148908,1510300484886250,4342466000000 +472,8299712464198532,1463134694007783,4343810000000 +473,8285364056959992,1468083519808458,4344488000000 +474,8271283701035530,1472947334083940,4344896000000 +475,8257181621486334,1477802105663126,4342424000000 +476,8243160250015058,1482630527975380,4342358000000 +477,8228975392411645,1487521974453746,4343538000000 +478,8214994323137691,1492348596544412,4344806000000 +479,8201129718611947,1497165313551009,4349418000000 +480,8187070476979147,1502038291741991,4352816000000 +481,8172897979043621,1506941118966234,4354472000000 +482,8158797579880842,1511816216675332,4357910000000 +483,8144675912835745,1516694508210497,4363494000000 +484,8130526817894675,1521584234785002,4363692000000 +485,8116368193817815,1526471044644533,4368316000000 +486,8102288065038969,1531331315641697,4369878000000 +487,8088437919200592,1536114019459084,4370430000000 +488,8074565826556158,1540899754794546,4368600000000 +489,8060828529222149,1545637399983834,4368728000000 +490,8047010081468560,1550401607761749,4368460000000 +491,8033172145504738,1555176058996221,4368708000000 +492,8019423454847030,1559918119894833,4369076000000 +493,8005883735416310,1564590697784215,4368902000000 +494,7992233702329513,1569327676418076,4371130000000 +495,7978730256074581,1574019542289399,4370532000000 +496,7965004592410008,1476619445875690,4371646000000 +497,7951386216629221,1481317014105581,4373812000000 +498,7937815230851221,1486007059665745,4374584000000 +499,7924170545862647,1490725021530841,4375222000000 +500,7910589154077874,1495403947921756,4374202000000 +501,7897001506315015,1500074612179231,4373828000000 +502,7883390526253263,1504763227266060,4371308000000 +503,7869740167788845,1509473319875466,4372838000000 +504,7856412006012711,1514131702485276,4372242000000 +505,7842889284966704,1518869032724501,4371596000000 +506,7829573342038784,1523532096285944,4371222000000 +507,7816251180544575,1528154947846618,4370916000000 +508,7802782712972544,1532828945705969,4372738000000 +509,7789809997399209,1537376975934454,4373648000000 +510,7776682557725743,1541947683854337,4372322000000 +511,7763466534897369,1546537960700287,4373496000000 +512,7750347819157560,1551120138892345,4374220000000 +513,7737238847635656,1555688467465569,4370458000000 +514,7724114066210540,1560274087294584,4368548000000 +515,7710840811535124,1564893381140133,4368824000000 +516,7697681960525046,1569476292253072,4368134000000 +517,7684715051166790,1574027226901630,4368934000000 +518,7671724883246433,1578692757476585,4369364000000 +519,7658581735905745,1583319123795732,4369934000000 +520,7645768723008280,1587845138859041,4367694000000 +521,7633094000777687,1592331641228984,4363744000000 +522,7620302863773952,1596870443401439,4360590000000 +523,7607610747076201,1601367031103590,4356218000000 +524,7595003223306721,1605867363924094,4349906000000 +525,7582597266023565,1610327841943309,4349940000000 +526,7570016745300604,1614845026268105,4343236000000 +527,7557630419439671,1619326627836757,4344122000000 +528,7545301818317184,1623777941276573,4336886000000 +529,7533008055566611,1628218659074146,4341094000000 +530,7520543391434932,1632716817250127,4343690000000 +531,7508122101528199,1637199823678645,4332432000000 +532,7495773911585373,1641656329184830,4331644000000 +533,7483332054370326,1646135439887110,4333806000000 +534,7470819003002992,1650631985932748,4333906000000 +535,7458430104616144,1655096893563009,4333608000000 +536,7445967855869507,1659589823833183,4336686000000 +537,7433694305914142,1664007113173757,4337082000000 +538,7421230710104429,1668494670404460,4339676000000 +539,7408926927280507,1672936804237521,4341324000000 +540,7396826608934598,1677291581661325,4345984000000 +541,7384491692705295,1681719994417422,4346574000000 +542,7372161971923491,1686145488917895,4348482000000 +543,7360009982247106,1690501545334536,4350230000000 +544,7347811044701520,1694874110569621,4350630000000 +545,7335771117346353,1699217928890605,4353058000000 +546,7323776141502598,1703528717038308,4354916000000 +547,7311783103862433,1707859322622122,4357554000000 +548,7299564319473914,1712261860261038,4358802000000 +549,7287372980420867,1716669382767858,4362866000000 +550,7275303870393836,1721027451020579,4364196000000 +551,7263297721984758,1726047077836483,4363670000000 +552,7251344361797924,1730343069063130,4365780000000 +553,7239343896065115,1734673185491213,4368228000000 +554,7227379990267374,1739008615838196,4368574000000 +555,7215372882539040,1743348402076197,4368160000000 +556,7203284394203835,1747689209080869,4369776000000 +557,7191306922028738,1751960844921027,4370742000000 +558,7179190548389205,1756292565163609,4371288000000 +559,7167304151917849,1760542592099957,4371692000000 +560,7155310770495321,1764858484971318,4371648000000 +561,7143512059809197,1769070988271220,4371886000000 +562,7131582407880455,1773324623715012,4372422000000 +563,7119603164653180,1777598306850688,4375000000000 +564,7107661935971911,1781861638625568,4377476000000 +565,7095848084245357,1786094570568060,4377458000000 +566,7084099836750283,1790306195001816,4378138000000 +567,7072264095076784,1794545873391900,4378582000000 +568,7060557087817263,1798748405131101,4380108000000 +569,7048808450033904,1802968321266151,4381570000000 +570,7036998224247014,1807198754088108,4381280000000 +571,7025299385648516,1809902011480628,4380476000000 +572,7013758119416311,1814040155030734,4380980000000 +573,7002095664211091,1818223498763715,4381870000000 +574,6990408282261570,1822441339817833,4382692000000 +575,6978937917905659,1658127995342197,4384506000000 +576,6967366713405925,1584160945084072,4386016000000 +577,6955875027747416,1570300038420910,4382426000000 +578,6944275078910457,1568521696870591,4384992000000 +579,6932775766859839,1572662239592268,4385628000000 diff --git a/modules/spo_state/src/state.rs b/modules/spo_state/src/state.rs index 49109b18..b76b67a0 100644 --- a/modules/spo_state/src/state.rs +++ b/modules/spo_state/src/state.rs @@ -45,6 +45,9 @@ pub struct State { #[serde_as(as = "SerializeMapAs")] spos: HashMap, PoolRegistration>, + #[serde_as(as = "SerializeMapAs")] + pending_updates: HashMap, PoolRegistration>, + #[serde_as(as = "SerializeMapAs<_, Vec>")] pending_deregistrations: HashMap>>, @@ -66,6 +69,7 @@ impl State { block: 0, epoch: 0, spos: HashMap::new(), + pending_updates: HashMap::new(), pending_deregistrations: HashMap::new(), vrf_key_to_pool_id_map: HashMap::new(), historical_spos: if config.store_historical_state() { @@ -96,6 +100,7 @@ impl From for State { block: 0, epoch: 0, spos, + pending_updates: value.updates.into(), pending_deregistrations, vrf_key_to_pool_id_map, historical_spos: None, @@ -108,6 +113,11 @@ impl From<&State> for SPOState { fn from(state: &State) -> Self { Self { pools: state.spos.iter().map(|(key, value)| (key.clone(), value.clone())).collect(), + updates: state + .pending_updates + .iter() + .map(|(key, value)| (key.clone(), value.clone())) + .collect(), retiring: state .pending_deregistrations .iter() @@ -203,6 +213,12 @@ impl State { // are still included let spos = self.spos.values().cloned().collect(); + // Update any pending + for (operator, reg) in &self.pending_updates { + self.spos.insert(operator.clone(), reg.clone()); + } + self.pending_updates.clear(); + // Deregister any pending let mut retired_spos: Vec = Vec::new(); let deregistrations = self.pending_deregistrations.remove(&self.epoch); @@ -233,13 +249,25 @@ impl State { } fn handle_pool_registration(&mut self, block: &BlockInfo, reg: &PoolRegistration) { - debug!( - block = block.number, - "Registering SPO {}", - hex::encode(®.operator) - ); - self.spos.insert(reg.operator.clone(), reg.clone()); - self.vrf_key_to_pool_id_map.insert(reg.vrf_key_hash.clone(), reg.operator.clone()); + // Insert or update the SPO + if self.spos.contains_key(®.operator) { + debug!( + block = block.number, + "New pending SPO update {} {:?}", + hex::encode(®.operator), + reg + ); + self.pending_updates.insert(reg.operator.clone(), reg.clone()); + } else { + debug!( + block = block.number, + "Registering SPO {} {:?}", + hex::encode(®.operator), + reg + ); + self.spos.insert(reg.operator.clone(), reg.clone()); + self.vrf_key_to_pool_id_map.insert(reg.vrf_key_hash.clone(), reg.operator.clone()); + } // Remove any existing queued deregistrations for (epoch, deregistrations) in &mut self.pending_deregistrations.iter_mut() { @@ -288,6 +316,10 @@ impl State { } } self.pending_deregistrations.entry(ret.epoch).or_default().push(ret.operator.clone()); + + // Note: not removing pending updates - the deregistation may happen many + // epochs later than the update, and we apply updates before deregistrations + // so they cannot recreate deregistered SPOs } } diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index c41f1db5..9bb7ab18 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -8,7 +8,7 @@ genesis-key = "5b3139312c36362c3134302c3138352c3133382c31312c3233372c3230372c323 # Download max age in hours. E.g. 8 means 8 hours (if there isn't any snapshot within this time range download from Mithril) download-max-age = "never" # Pause constraint E.g. "epoch:100", "block:1200" -pause = "none" +#pause = "epoch:213" [module.upstream-chain-fetcher] sync-point = "snapshot" @@ -71,6 +71,9 @@ write-full-cache = "false" store-history = false [module.accounts-state] +# Verify against captured CSV +verify-pots-file = "../../modules/accounts_state/test-data/pots.mainnet.csv" +verify-rewards-files = "../../modules/accounts_state/test-data/rewards.mainnet.{}.csv" [module.assets-state]