From 3e14b7507c661a58b44a1ddf3e7bf35d892e8fa9 Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Tue, 22 Jun 2021 21:32:03 -0300 Subject: [PATCH] explorer: stake control and implement public tally this could re-use more code from the ledger, but it's not that easy to ignore validations and also I think this is simple enough for now. The implementation is somewhat isolated, so it's not that hard to change things after some more thought. --- explorer/src/api/graphql/mod.rs | 8 +-- explorer/src/api/graphql/scalars.rs | 6 +- explorer/src/db/indexing.rs | 15 ++++- explorer/src/db/mod.rs | 100 +++++++++++++++++++++++++--- explorer/src/db/tally.rs | 44 ++++++++++++ 5 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 explorer/src/db/tally.rs diff --git a/explorer/src/api/graphql/mod.rs b/explorer/src/api/graphql/mod.rs index e66245a525..48e48a0d64 100644 --- a/explorer/src/api/graphql/mod.rs +++ b/explorer/src/api/graphql/mod.rs @@ -497,14 +497,14 @@ impl Block { async fn fetch_explorer_block(&self, db: &ExplorerDb) -> FieldResult> { let mut contents = self.contents.lock().await; if let Some(block) = &*contents { - return Ok(Arc::clone(&block)); + Ok(Arc::clone(&block)) } else { let block = db.get_block(&self.hash).await.ok_or_else(|| { ApiError::InternalError("Couldn't find block's contents in explorer".to_owned()) })?; *contents = Some(Arc::clone(&block)); - return Ok(block); + Ok(block) } } @@ -1373,14 +1373,14 @@ impl VotePlanStatus { tally: proposal.tally.map(|tally| match tally { ExplorerVoteTally::Public { results, options } => { TallyStatus::Public(TallyPublicStatus { - results: results.into_iter().map(Into::into).collect(), + results: results.iter().map(Into::into).collect(), options: options.into(), }) } ExplorerVoteTally::Private { results, options } => { TallyStatus::Private(TallyPrivateStatus { results: results - .map(|res| res.into_iter().map(Into::into).collect()), + .map(|res| res.iter().map(Into::into).collect()), options: options.into(), }) } diff --git a/explorer/src/api/graphql/scalars.rs b/explorer/src/api/graphql/scalars.rs index 3765c4fac2..363fab84cc 100644 --- a/explorer/src/api/graphql/scalars.rs +++ b/explorer/src/api/graphql/scalars.rs @@ -388,9 +388,9 @@ impl From for ExternalPro } } -impl From for Weight { - fn from(w: vote::Weight) -> Self { - Self(format!("{}", w)) +impl> From for Weight { + fn from(w: T) -> Self { + Self(format!("{}", w.borrow())) } } diff --git a/explorer/src/db/indexing.rs b/explorer/src/db/indexing.rs index 37d732bc3e..1df9a76f86 100644 --- a/explorer/src/db/indexing.rs +++ b/explorer/src/db/indexing.rs @@ -3,6 +3,7 @@ use cardano_legacy_address::Addr as OldAddress; use chain_addr::{Address, Discrimination}; use chain_core::property::{Block as _, Fragment as _}; use chain_impl_mockchain::{ + account::Identifier, block::{Block, Proof}, certificate::{ Certificate, ExternalProposalId, PoolId, PoolRegistration, PoolRetirement, VotePlanId, @@ -131,7 +132,7 @@ pub struct ExplorerVoteProposal { #[derive(Clone)] pub enum ExplorerVoteTally { Public { - results: Vec, + results: Box<[Weight]>, options: Options, }, Private { @@ -446,3 +447,15 @@ impl ExplorerTransaction { &self.outputs } } + +impl ExplorerAddress { + pub fn to_single_account(&self) -> Option { + match self { + ExplorerAddress::New(address) => match address.kind() { + chain_addr::Kind::Single(key) => Some(key.clone().into()), + _ => None, + }, + ExplorerAddress::Old(_) => None, + } + } +} diff --git a/explorer/src/db/mod.rs b/explorer/src/db/mod.rs index 661461571e..00ebf72f8d 100644 --- a/explorer/src/db/mod.rs +++ b/explorer/src/db/mod.rs @@ -2,7 +2,10 @@ pub mod error; pub mod indexing; pub mod multiverse; pub mod persistent_sequence; +mod tally; +use crate::db::tally::compute_public_tally; +use crate::db::tally::compute_private_tally; use self::error::{ExplorerError as Error, Result}; use self::indexing::{ Addresses, Blocks, ChainLengths, EpochData, Epochs, ExplorerAddress, ExplorerBlock, @@ -12,10 +15,12 @@ use self::indexing::{ use self::persistent_sequence::PersistentSequence; use chain_core::property::Block as _; pub use multiverse::Ref; - use chain_addr::Discrimination; -use chain_impl_mockchain::fee::LinearFee; -use chain_impl_mockchain::{block::HeaderId as HeaderHash, certificate::VotePlanId}; +use chain_impl_mockchain::{ + block::HeaderId as HeaderHash, + certificate::VotePlanId, + stake::{Stake, StakeControl}, +}; use chain_impl_mockchain::{ block::{Block, ChainLength, Epoch}, certificate::{Certificate, PoolId}, @@ -23,6 +28,7 @@ use chain_impl_mockchain::{ config::ConfigParam, fragment::{ConfigParams, Fragment, FragmentId}, }; +use chain_impl_mockchain::{fee::LinearFee, vote::PayloadType}; use futures::prelude::*; use multiverse::Multiverse; use std::convert::Infallible; @@ -83,6 +89,7 @@ pub struct State { stake_pool_data: StakePool, stake_pool_blocks: StakePoolBlocks, vote_plans: VotePlans, + stake_control: StakeControl, } #[derive(Clone)] @@ -123,7 +130,8 @@ impl ExplorerDb { let addresses = apply_block_to_addresses(Addresses::new(), &block); let (stake_pool_data, stake_pool_blocks) = apply_block_to_stake_pools(StakePool::new(), StakePoolBlocks::new(), &block); - let vote_plans = apply_block_to_vote_plans(VotePlans::new(), &block); + let stake_control = apply_block_to_stake_control(StakeControl::new(), &block); + let vote_plans = apply_block_to_vote_plans(VotePlans::new(), &block, &stake_control); let initial_state = State { transactions, @@ -134,6 +142,7 @@ impl ExplorerDb { stake_pool_data, stake_pool_blocks, vote_plans, + stake_control, }; let block0_id = block0.id(); @@ -181,6 +190,7 @@ impl ExplorerDb { stake_pool_data, stake_pool_blocks, vote_plans, + stake_control, } = previous_state.state().clone(); let explorer_block = ExplorerBlock::resolve_from( @@ -194,6 +204,8 @@ impl ExplorerDb { let (stake_pool_data, stake_pool_blocks) = apply_block_to_stake_pools(stake_pool_data, stake_pool_blocks, &explorer_block); + let stake_control = apply_block_to_stake_control(stake_control, &explorer_block); + let state_ref = multiverse .insert( chain_length, @@ -207,7 +219,12 @@ impl ExplorerDb { chain_lengths: apply_block_to_chain_lengths(chain_lengths, &explorer_block)?, stake_pool_data, stake_pool_blocks, - vote_plans: apply_block_to_vote_plans(vote_plans, &explorer_block), + vote_plans: apply_block_to_vote_plans( + vote_plans, + &explorer_block, + &stake_control, + ), + stake_control, }, ) .await; @@ -568,7 +585,11 @@ fn apply_block_to_stake_pools( (data, blocks) } -fn apply_block_to_vote_plans(mut vote_plans: VotePlans, block: &ExplorerBlock) -> VotePlans { +fn apply_block_to_vote_plans( + mut vote_plans: VotePlans, + block: &ExplorerBlock, + stake: &StakeControl, +) -> VotePlans { for tx in block.transactions.values() { if let Some(cert) = &tx.certificate { vote_plans = match cert { @@ -655,9 +676,27 @@ fn apply_block_to_vote_plans(mut vote_plans: VotePlans, block: &ExplorerBlock) - .unwrap(), } } - Certificate::VoteTally(_vote_tally) => { - unimplemented!("this may require access to the node's Tip"); - } + Certificate::VoteTally(vote_tally) => vote_plans + .update(vote_tally.id(), |vote_plan| { + let proposals = vote_plan + .proposals + .clone() + .into_iter() + .map(|mut proposal| { + proposal.tally = Some(match vote_tally.tally_type() { + PayloadType::Public => compute_public_tally(&proposal, stake), + PayloadType::Private => compute_private_tally(&proposal), + }); + proposal + }) + .collect(); + let vote_plan = ExplorerVotePlan { + proposals, + ..(**vote_plan).clone() + }; + Ok::<_, std::convert::Infallible>(Some(Arc::new(vote_plan))) + }) + .unwrap(), _ => vote_plans, } } @@ -666,6 +705,49 @@ fn apply_block_to_vote_plans(mut vote_plans: VotePlans, block: &ExplorerBlock) - vote_plans } +fn apply_block_to_stake_control( + mut stake_control: StakeControl, + block: &ExplorerBlock, +) -> StakeControl { + for (_id, tx) in block.transactions.iter() { + // TODO: there is a bit of code duplication here (maybe?) + + for input in tx.inputs() { + let indexing::ExplorerInput { address, value } = input; + let address = match address { + ExplorerAddress::Old(_) => continue, + ExplorerAddress::New(address) => address, + }; + + match address.kind() { + chain_addr::Kind::Group(_, id) | chain_addr::Kind::Account(id) => { + stake_control = + stake_control.remove_from(id.clone().into(), Stake::from_value(*value)); + } + _ => continue, + } + } + + for output in tx.outputs() { + let indexing::ExplorerOutput { address, value } = output; + let address = match address { + ExplorerAddress::Old(_) => continue, + ExplorerAddress::New(address) => address, + }; + + match address.kind() { + chain_addr::Kind::Group(_, id) | chain_addr::Kind::Account(id) => { + stake_control = + stake_control.add_to(id.clone().into(), Stake::from_value(*value)); + } + _ => continue, + } + } + } + + stake_control +} + impl BlockchainConfig { fn from_config_params(params: &ConfigParams) -> BlockchainConfig { let mut discrimination: Option = None; diff --git a/explorer/src/db/tally.rs b/explorer/src/db/tally.rs new file mode 100644 index 0000000000..5a9362ef47 --- /dev/null +++ b/explorer/src/db/tally.rs @@ -0,0 +1,44 @@ +use chain_impl_mockchain::stake::StakeControl; + +use super::indexing::ExplorerVoteProposal; +use crate::db::indexing::{ExplorerVote, ExplorerVoteTally}; + +pub fn compute_private_tally(proposal: &ExplorerVoteProposal) -> ExplorerVoteTally { + tracing::warn!("private tally triggered but it is not implemented"); + + ExplorerVoteTally::Private { + results: None, + options: proposal.options.clone(), + } +} + +pub fn compute_public_tally( + proposal: &ExplorerVoteProposal, + stake: &StakeControl, +) -> ExplorerVoteTally { + let mut results = vec![0u64; proposal.options.choice_range().end as usize]; + + for (address, vote) in proposal.votes.iter() { + if let Some(account_id) = address.to_single_account() { + if let Some(stake) = stake.by(&account_id) { + match vote.as_ref() { + ExplorerVote::Public(choice) => { + let index = choice.as_byte() as usize; + results[index] = results[index].saturating_add(stake.into()); + } + ExplorerVote::Private { + proof: _, + encrypted_vote: _, + } => { + unreachable!("internal error: found private vote when computing tally for public proposal") + } + } + } + } + } + + ExplorerVoteTally::Public { + results: results.drain(..).map(u64::into).collect(), + options: proposal.options.clone(), + } +}