From a8845483eccffca6a25c20ace6220f4e83a11cdf Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Mon, 31 May 2021 16:48:20 -0300 Subject: [PATCH] add vote plans minus VoteTally VoteTally handling currently requires access to the node (with the current implementation, at least) --- Cargo.lock | 1 + explorer/Cargo.toml | 1 + explorer/src/api/graphql/mod.rs | 365 +++++++++++++++++++++++++++- explorer/src/api/graphql/scalars.rs | 1 + explorer/src/db/indexing.rs | 47 +++- explorer/src/db/mod.rs | 130 +++++++++- explorer/src/main.rs | 2 +- 7 files changed, 531 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54cfbd7eda..53eca6332f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1141,6 +1141,7 @@ dependencies = [ "async-graphql", "async-graphql-warp", "backoff", + "base64 0.13.0", "cardano-legacy-address", "chain-addr", "chain-core", diff --git a/explorer/Cargo.toml b/explorer/Cargo.toml index 3a93324b48..f9bc947eb5 100644 --- a/explorer/Cargo.toml +++ b/explorer/Cargo.toml @@ -37,6 +37,7 @@ tonic = "0.4" multiaddr = { package = "parity-multiaddr", version = "0.11" } rand = "0.8.3" rand_chacha = "0.3.0" +base64 = "0.13.0" jormungandr-lib = {path = "../jormungandr-lib"} diff --git a/explorer/src/api/graphql/mod.rs b/explorer/src/api/graphql/mod.rs index 23a83e9604..e66245a525 100644 --- a/explorer/src/api/graphql/mod.rs +++ b/explorer/src/api/graphql/mod.rs @@ -8,26 +8,32 @@ use async_graphql::{ Context, EmptyMutation, FieldError, FieldResult, Object, SimpleObject, Subscription, }; -use self::connections::{ - compute_interval, ConnectionFields, InclusivePaginationInterval, PaginationInterval, - ValidatedPaginationArguments, -}; -use self::error::ApiError; use self::scalars::{ - BlockCount, ChainLength, EpochNumber, ExternalProposalId, IndexCursor, NonZero, PoolCount, - PoolId, PublicKey, Slot, TransactionCount, Value, VoteOptionRange, + BlockCount, ChainLength, EpochNumber, ExternalProposalId, IndexCursor, NonZero, PayloadType, + PoolCount, PoolId, PublicKey, Slot, TransactionCount, Value, VoteOptionRange, + VotePlanStatusCount, +}; +use self::{ + connections::{ + compute_interval, ConnectionFields, InclusivePaginationInterval, PaginationInterval, + ValidatedPaginationArguments, + }, + scalars::VotePlanId, }; +use self::{error::ApiError, scalars::Weight}; use crate::db::indexing::{ - BlockProducer, EpochData, ExplorerAddress, ExplorerBlock, ExplorerTransaction, StakePoolData, + BlockProducer, EpochData, ExplorerAddress, ExplorerBlock, ExplorerTransaction, ExplorerVote, + ExplorerVotePlan, ExplorerVoteTally, StakePoolData, }; use crate::db::persistent_sequence::PersistentSequence; use crate::db::{ExplorerDb, Settings as ChainSettings}; use cardano_legacy_address::Addr as OldAddress; use certificates::*; -use chain_impl_mockchain::block::{ - BlockDate as InternalBlockDate, Epoch as InternalEpoch, HeaderId as HeaderHash, -}; use chain_impl_mockchain::key::BftLeaderId; +use chain_impl_mockchain::{ + block::{BlockDate as InternalBlockDate, Epoch as InternalEpoch, HeaderId as HeaderHash}, + vote::{EncryptedVote, ProofOfCorrectVote}, +}; use chain_impl_mockchain::{certificate, fragment::FragmentId}; use std::convert::{TryFrom, TryInto}; use std::str::FromStr; @@ -200,6 +206,86 @@ impl Branch { .await } + pub async fn all_vote_plans( + &self, + first: Option, + last: Option, + before: Option, + after: Option, + ) -> FieldResult< + Connection, EmptyFields>, + > { + let mut vote_plans = self.state.state().get_vote_plans(); + + vote_plans.sort_unstable_by_key(|(id, _data)| id.clone()); + + query( + after, + before, + first, + last, + |after, before, first, last| async move { + let boundaries = if !vote_plans.is_empty() { + PaginationInterval::Inclusive(InclusivePaginationInterval { + lower_bound: 0u32, + upper_bound: vote_plans + .len() + .checked_sub(1) + .unwrap() + .try_into() + .expect("tried to paginate more than 2^32 elements"), + }) + } else { + PaginationInterval::Empty + }; + + let pagination_arguments = ValidatedPaginationArguments { + first, + last, + before: before.map(u32::try_from).transpose()?, + after: after.map(u32::try_from).transpose()?, + }; + + let (range, page_meta) = compute_interval(boundaries, pagination_arguments)?; + let mut connection = Connection::with_additional_fields( + page_meta.has_previous_page, + page_meta.has_next_page, + ConnectionFields { + total_count: page_meta.total_count, + }, + ); + + let edges = match range { + PaginationInterval::Empty => vec![], + PaginationInterval::Inclusive(range) => { + let from = range.lower_bound; + let to = range.upper_bound; + + (from..=to) + .map(|i: u32| { + let (_pool_id, vote_plan_data) = + &vote_plans[usize::try_from(i).unwrap()]; + ( + VotePlanStatus::vote_plan_from_data(Arc::clone(vote_plan_data)), + i, + ) + }) + .collect::>() + } + }; + + connection.append( + edges + .iter() + .map(|(vps, cursor)| Edge::new(IndexCursor::from(*cursor), vps.clone())), + ); + + Ok(connection) + }, + ) + .await + } + pub async fn all_stake_pools( &self, first: Option, @@ -1175,6 +1261,255 @@ pub struct PoolStakeDistribution { delegated_stake: Value, } +#[derive(Clone)] +pub struct VotePayloadPublicStatus { + choice: i32, +} + +#[derive(Clone)] +pub struct VotePayloadPrivateStatus { + proof: ProofOfCorrectVote, + encrypted_vote: EncryptedVote, +} + +#[Object] +impl VotePayloadPublicStatus { + pub async fn choice(&self, _context: &Context<'_>) -> i32 { + self.choice + } +} + +#[Object] +impl VotePayloadPrivateStatus { + pub async fn proof(&self, _context: &Context<'_>) -> String { + let bytes_proof = self.proof.serialize(); + base64::encode_config(bytes_proof, base64::URL_SAFE) + } + + pub async fn encrypted_vote(&self, _context: &Context<'_>) -> String { + let encrypted_bote_bytes = self.encrypted_vote.serialize(); + base64::encode_config(encrypted_bote_bytes, base64::URL_SAFE) + } +} + +#[derive(Clone, async_graphql::Union)] +pub enum VotePayloadStatus { + Public(VotePayloadPublicStatus), + Private(VotePayloadPrivateStatus), +} + +// TODO do proper vote tally +#[derive(Clone, SimpleObject)] +pub struct TallyPublicStatus { + results: Vec, + options: VoteOptionRange, +} + +#[derive(Clone, SimpleObject)] +pub struct TallyPrivateStatus { + results: Option>, + options: VoteOptionRange, +} + +#[derive(Clone, async_graphql::Union)] +pub enum TallyStatus { + Public(TallyPublicStatus), + Private(TallyPrivateStatus), +} + +#[derive(Clone, SimpleObject)] +pub struct VotePlanStatus { + id: VotePlanId, + vote_start: BlockDate, + vote_end: BlockDate, + committee_end: BlockDate, + payload_type: PayloadType, + proposals: Vec, +} + +impl VotePlanStatus { + pub async fn vote_plan_from_id( + vote_plan_id: VotePlanId, + context: &Context<'_>, + ) -> FieldResult { + let vote_plan_id = chain_impl_mockchain::certificate::VotePlanId::from_str(&vote_plan_id.0) + .map_err(|err| -> FieldError { ApiError::InvalidAddress(err.to_string()).into() })?; + if let Some(vote_plan) = extract_context(&context) + .db + .get_vote_plan_by_id(&vote_plan_id) + .await + { + return Ok(Self::vote_plan_from_data(vote_plan)); + } + + Err(ApiError::NotFound(format!( + "Vote plan with id {} not found", + vote_plan_id.to_string() + )) + .into()) + } + + pub fn vote_plan_from_data(vote_plan: Arc) -> Self { + let ExplorerVotePlan { + id, + vote_start, + vote_end, + committee_end, + payload_type, + proposals, + } = (*vote_plan).clone(); + + VotePlanStatus { + id: VotePlanId::from(id), + vote_start: BlockDate::from(vote_start), + vote_end: BlockDate::from(vote_end), + committee_end: BlockDate::from(committee_end), + payload_type: PayloadType::from(payload_type), + proposals: proposals + .into_iter() + .map(|proposal| VoteProposalStatus { + proposal_id: ExternalProposalId::from(proposal.proposal_id), + options: VoteOptionRange::from(proposal.options), + tally: proposal.tally.map(|tally| match tally { + ExplorerVoteTally::Public { results, options } => { + TallyStatus::Public(TallyPublicStatus { + results: results.into_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()), + options: options.into(), + }) + } + }), + votes: proposal + .votes + .iter() + .map(|(key, vote)| match vote.as_ref() { + ExplorerVote::Public(choice) => VoteStatus { + address: key.into(), + payload: VotePayloadStatus::Public(VotePayloadPublicStatus { + choice: choice.as_byte().into(), + }), + }, + ExplorerVote::Private { + proof, + encrypted_vote, + } => VoteStatus { + address: key.into(), + payload: VotePayloadStatus::Private(VotePayloadPrivateStatus { + proof: proof.clone(), + encrypted_vote: encrypted_vote.clone(), + }), + }, + }) + .collect(), + }) + .collect(), + } + } +} + +#[derive(Clone, SimpleObject)] +pub struct VoteStatus { + address: Address, + payload: VotePayloadStatus, +} + +#[derive(Clone)] +pub struct VoteProposalStatus { + proposal_id: ExternalProposalId, + options: VoteOptionRange, + tally: Option, + votes: Vec, +} + +#[Object] +impl VoteProposalStatus { + pub async fn proposal_id(&self) -> &ExternalProposalId { + &self.proposal_id + } + + pub async fn options(&self) -> &VoteOptionRange { + &self.options + } + + pub async fn tally(&self) -> Option<&TallyStatus> { + self.tally.as_ref() + } + + pub async fn votes( + &self, + first: Option, + last: Option, + before: Option, + after: Option, + ) -> FieldResult, EmptyFields>> { + query( + after, + before, + first, + last, + |after, before, first, last| async move { + let boundaries = if !self.votes.is_empty() { + PaginationInterval::Inclusive(InclusivePaginationInterval { + lower_bound: 0u32, + upper_bound: self + .votes + .len() + .checked_sub(1) + .unwrap() + .try_into() + .expect("tried to paginate more than 2^32 elements"), + }) + } else { + PaginationInterval::Empty + }; + + let pagination_arguments = ValidatedPaginationArguments { + first, + last, + before: before.map(u32::try_from).transpose()?, + after: after.map(u32::try_from).transpose()?, + }; + + let (range, page_meta) = compute_interval(boundaries, pagination_arguments)?; + let mut connection = Connection::with_additional_fields( + page_meta.has_previous_page, + page_meta.has_next_page, + ConnectionFields { + total_count: page_meta.total_count, + }, + ); + + let edges = match range { + PaginationInterval::Empty => vec![], + PaginationInterval::Inclusive(range) => { + let from = range.lower_bound; + let to = range.upper_bound; + + (from..=to) + .map(|i: u32| (self.votes[i as usize].clone(), i)) + .collect::>() + } + }; + + connection.append( + edges + .iter() + .map(|(vs, cursor)| Edge::new(IndexCursor::from(*cursor), vs.clone())), + ); + + Ok(connection) + }, + ) + .await + } +} + pub struct Query; #[Object] @@ -1245,6 +1580,14 @@ impl Query { pub async fn settings(&self, _context: &Context<'_>) -> FieldResult { Ok(Settings {}) } + + pub async fn vote_plan( + &self, + context: &Context<'_>, + id: String, + ) -> FieldResult { + VotePlanStatus::vote_plan_from_id(VotePlanId(id), context).await + } } pub struct Subscription; diff --git a/explorer/src/api/graphql/scalars.rs b/explorer/src/api/graphql/scalars.rs index e15c02b6b4..3765c4fac2 100644 --- a/explorer/src/api/graphql/scalars.rs +++ b/explorer/src/api/graphql/scalars.rs @@ -110,6 +110,7 @@ impl ScalarType for Value { pub type BlockCount = u64; pub type TransactionCount = u64; pub type PoolCount = u64; +pub type VotePlanStatusCount = u64; pub struct PublicKey(pub String); diff --git a/explorer/src/db/indexing.rs b/explorer/src/db/indexing.rs index 25edb5d5dd..afd5b7f73f 100644 --- a/explorer/src/db/indexing.rs +++ b/explorer/src/db/indexing.rs @@ -4,7 +4,9 @@ use chain_addr::{Address, Discrimination}; use chain_core::property::{Block as _, Fragment as _}; use chain_impl_mockchain::{ block::{Block, Proof}, - certificate::{Certificate, PoolId, PoolRegistration, PoolRetirement}, + certificate::{ + Certificate, ExternalProposalId, PoolId, PoolRegistration, PoolRetirement, VotePlanId, + }, fragment::{Fragment, FragmentId}, header::BlockDate, header::ChainLength, @@ -13,6 +15,7 @@ use chain_impl_mockchain::{ key::BftLeaderId, transaction::{InputEnum, TransactionSlice, Witness}, value::Value, + vote::{Choice, EncryptedVote, Options, PayloadType, ProofOfCorrectVote, Weight}, }; use std::{ collections::{hash_map::DefaultHasher, HashMap}, @@ -32,6 +35,8 @@ pub type Epochs = Hamt; pub type StakePoolBlocks = Hamt>; pub type StakePool = Hamt; +pub type VotePlans = Hamt; + #[derive(Clone)] pub struct StakePoolData { pub registration: PoolRegistration, @@ -95,6 +100,46 @@ pub enum ExplorerAddress { Old(OldAddress), } +#[derive(Clone)] +pub struct ExplorerVotePlan { + pub id: VotePlanId, + pub vote_start: BlockDate, + pub vote_end: BlockDate, + pub committee_end: BlockDate, + pub payload_type: PayloadType, + pub proposals: Vec, +} + +#[derive(Clone)] +pub enum ExplorerVote { + Public(Choice), + Private { + proof: ProofOfCorrectVote, + encrypted_vote: EncryptedVote, + }, +} + +#[derive(Clone)] +pub struct ExplorerVoteProposal { + pub proposal_id: ExternalProposalId, + pub options: Options, + pub tally: Option, + pub votes: Hamt, +} + +// TODO do proper vote tally +#[derive(Clone)] +pub enum ExplorerVoteTally { + Public { + results: Vec, + options: Options, + }, + Private { + results: Option>, + options: Options, + }, +} + pub struct ExplorerBlockBuildingContext<'a> { pub discrimination: Discrimination, pub prev_transactions: &'a Transactions, diff --git a/explorer/src/db/mod.rs b/explorer/src/db/mod.rs index 16cc3d8b62..5dc2f7dfc6 100644 --- a/explorer/src/db/mod.rs +++ b/explorer/src/db/mod.rs @@ -5,16 +5,17 @@ pub mod persistent_sequence; use self::error::{ExplorerError as Error, Result}; use self::indexing::{ - Addresses, Blocks, ChainLengths, EpochData, Epochs, ExplorerAddress, ExplorerBlock, StakePool, - StakePoolBlocks, StakePoolData, Transactions, + Addresses, Blocks, ChainLengths, EpochData, Epochs, ExplorerAddress, ExplorerBlock, + ExplorerVote, ExplorerVotePlan, ExplorerVoteProposal, StakePool, StakePoolBlocks, + StakePoolData, Transactions, VotePlans, }; use self::persistent_sequence::PersistentSequence; use chain_core::property::Block as _; pub use multiverse::Ref; use chain_addr::Discrimination; -use chain_impl_mockchain::block::HeaderId as HeaderHash; use chain_impl_mockchain::fee::LinearFee; +use chain_impl_mockchain::{block::HeaderId as HeaderHash, certificate::VotePlanId}; use chain_impl_mockchain::{ block::{Block, ChainLength, Epoch}, certificate::{Certificate, PoolId}, @@ -81,6 +82,7 @@ pub struct State { chain_lengths: ChainLengths, stake_pool_data: StakePool, stake_pool_blocks: StakePoolBlocks, + vote_plans: VotePlans, } #[derive(Clone)] @@ -122,6 +124,7 @@ 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 initial_state = State { transactions, @@ -131,6 +134,7 @@ impl ExplorerDb { addresses, stake_pool_data, stake_pool_blocks, + vote_plans, }; let block0_id = block0.id(); @@ -177,6 +181,7 @@ impl ExplorerDb { chain_lengths, stake_pool_data, stake_pool_blocks, + vote_plans, } = previous_state.state().clone(); let explorer_block = ExplorerBlock::resolve_from( @@ -203,6 +208,7 @@ 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), }, ) .await; @@ -381,6 +387,19 @@ impl ExplorerDb { None } + pub async fn get_vote_plan_by_id( + &self, + vote_plan_id: &VotePlanId, + ) -> Option> { + for (_hash, state_ref) in self.multiverse.tips().await.iter() { + if let Some(b) = state_ref.state().vote_plans.lookup(&vote_plan_id) { + return Some(Arc::clone(b)); + } + } + + None + } + pub async fn get_branch(&self, hash: &HeaderHash) -> Option { self.multiverse.get_ref(hash).await } @@ -550,6 +569,104 @@ fn apply_block_to_stake_pools( (data, blocks) } +fn apply_block_to_vote_plans(mut vote_plans: VotePlans, block: &ExplorerBlock) -> VotePlans { + for tx in block.transactions.values() { + if let Some(cert) = &tx.certificate { + vote_plans = match cert { + Certificate::VotePlan(vote_plan) => vote_plans + .insert( + vote_plan.to_id(), + Arc::new(ExplorerVotePlan { + id: vote_plan.to_id(), + vote_start: vote_plan.vote_start(), + vote_end: vote_plan.vote_end(), + committee_end: vote_plan.committee_end(), + payload_type: vote_plan.payload_type(), + proposals: vote_plan + .proposals() + .iter() + .map(|proposal| ExplorerVoteProposal { + proposal_id: proposal.external_id().clone(), + options: proposal.options().clone(), + tally: None, + votes: Default::default(), + }) + .collect(), + }), + ) + .unwrap(), + Certificate::VoteCast(vote_cast) => { + use chain_impl_mockchain::vote::Payload; + let voter = tx.inputs[0].address.clone(); + match vote_cast.payload() { + Payload::Public { choice } => vote_plans + .update(vote_cast.vote_plan(), |vote_plan| { + let mut proposals = vote_plan.proposals.clone(); + proposals[vote_cast.proposal_index() as usize].votes = proposals + [vote_cast.proposal_index() as usize] + .votes + .insert_or_update( + voter, + Arc::new(ExplorerVote::Public(*choice)), + |_| { + Ok::<_, std::convert::Infallible>(Some(Arc::new( + ExplorerVote::Public(*choice), + ))) + }, + ) + .unwrap(); + let vote_plan = ExplorerVotePlan { + proposals, + ..(**vote_plan).clone() + }; + Ok::<_, std::convert::Infallible>(Some(Arc::new(vote_plan))) + }) + .unwrap(), + Payload::Private { + proof, + encrypted_vote, + } => vote_plans + .update(vote_cast.vote_plan(), |vote_plan| { + let mut proposals = vote_plan.proposals.clone(); + proposals[vote_cast.proposal_index() as usize].votes = proposals + [vote_cast.proposal_index() as usize] + .votes + .insert_or_update( + voter, + Arc::new(ExplorerVote::Private { + proof: proof.clone(), + encrypted_vote: encrypted_vote.clone(), + }), + |_| { + Ok::<_, std::convert::Infallible>(Some(Arc::new( + ExplorerVote::Private { + proof: proof.clone(), + encrypted_vote: encrypted_vote.clone(), + }, + ))) + }, + ) + .unwrap(); + let vote_plan = ExplorerVotePlan { + proposals, + ..(**vote_plan).clone() + }; + Ok::<_, std::convert::Infallible>(Some(Arc::new(vote_plan))) + }) + .unwrap(), + } + } + Certificate::VoteTally(_vote_tally) => { + unimplemented!("this may require access to the node's Tip"); + } + _ => vote_plans, + } + } + } + + vote_plans +} + impl BlockchainConfig { fn from_config_params(params: &ConfigParams) -> BlockchainConfig { let mut discrimination: Option = None; @@ -597,6 +714,13 @@ impl Tip { } impl State { + pub fn get_vote_plans(&self) -> Vec<(VotePlanId, Arc)> { + self.vote_plans + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + } + pub fn get_stake_pools(&self) -> Vec<(PoolId, Arc)> { self.stake_pool_data .iter() diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 3016459948..83f51fe329 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,5 +1,5 @@ mod api; -mod db; +pub mod db; mod indexer; mod settings;