Skip to content

Commit

Permalink
/poll-info REST end-point for retrieving estimated mixing time
Browse files Browse the repository at this point in the history
  • Loading branch information
shamsasari committed Aug 16, 2024
1 parent 6a5e95f commit c2daa11
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 37 deletions.
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ resolver = "2"
[workspace.package]
homepage = "https://projectglove.io/"
repository = "https://github.com/projectglove/glove-monorepo/"
version = "0.0.10"
version = "0.0.11"

[workspace.dependencies]
anyhow = "1.0.86"
Expand All @@ -29,7 +29,7 @@ serde = { version = "1.0.203", features = ["derive"] }
serde_bytes = "0.11.14"
serde_cbor = "0.11.2"
serde_json = "1.0.117"
serde_with = { version = "3.8.1", features = ["base64"] }
serde_with = "3.8.1"
parity-scale-codec = "3.6.12"
bigdecimal = "0.4.3"
clap = { version = "4.5.4", features = ["derive"] }
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,41 @@ If request body is invalid then a `422 Unprocessable Entity` is returned. If the
request itself then a `400 Bad Request` is returned with a JSON object containing the error type (`error`) and
description (`description`). `429 Too Many Requests` can also be returned and the request should be retried later.

## `GET /poll-info/{poll_index}`

Get Glove-specific information about poll with index `poll_index`.

### Request

None

### Response

A JSON object with the following fields:

#### `mixing_time`

**Estimated** time the Glove service will mix vote requests and submit them on-chain. If the service hasn't received
requests for this poll then this is the time the service _would_ mix if it had. This field is only available for polls
which have reached the decision phase. The mixing time isn't fixed and may change as the poll progresses. It is thus
recommended to occasionally poll this end-point.

The time is given in terms of both block number and UNIX timestamp seconds; the value is an object with fields
`block_number` and `timestamp`.

If the poll is not active then `400 Bad Request` is returned.

#### Example

```json
{
"mixing_time": {
"block_number": 22508946,
"timestamp": 1726170438
}
}
```

# Client CLI

There is a CLI client for interacting with the Glove service from the command line. It is built alongside the Glove
Expand Down
9 changes: 9 additions & 0 deletions client-interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,15 @@ impl SubstrateNetwork {
.ok_or_else(|| SubxtError::Other("Current block number not available".into()))?;
Ok(current_block_number)
}

pub async fn current_time(&self) -> Result<u64, SubxtError> {
let current_time = self.api
.storage()
.at_latest().await?
.fetch(&storage().timestamp().now()).await?
.ok_or_else(|| SubxtError::Other("Current time not available".into()))?;
Ok(current_time)
}
}

impl Debug for SubstrateNetwork {
Expand Down
2 changes: 1 addition & 1 deletion devops/ansible/roles/glove/templates/glove.service
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Description=Glove Nitro service
After=network.target

[Service]
ExecStart=/usr/local/glove/service --proxy-secret-phrase {{ glove_proxy_secret_phrase }} --address {{ glove_address }} --node-endpoint {{ glove_node_endpoint }} --regular-mix {% if glove_db == "dynamodb" %}
ExecStart=/usr/local/glove/service --proxy-secret-phrase {{ glove_proxy_secret_phrase }} --address {{ glove_address }} --node-endpoint {{ glove_node_endpoint }} {% if glove_db == "dynamodb" %}
dynamodb --table-name {{ glove_db_table }}
{% else %}
in-memory
Expand Down
42 changes: 41 additions & 1 deletion service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use sp_runtime::AccountId32;
use tokio::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::warn;

use client_interface::{account_to_subxt_multi_address, CallableSubstrateNetwork, subscan};
use client_interface::{account_to_subxt_multi_address, CallableSubstrateNetwork, ReferendumStatus, subscan, SubstrateNetwork};
use client_interface::metadata::runtime_types::pallet_conviction_voting::pallet::Call as ConvictionVotingCall;
use client_interface::metadata::runtime_types::pallet_conviction_voting::vote::{AccountVote, Vote};
use client_interface::metadata::runtime_types::pallet_proxy::pallet::Call as ProxyCall;
Expand All @@ -26,6 +26,46 @@ pub mod mixing;
pub mod dynamodb;
pub mod storage;

pub const BLOCK_TIME_SECS: u32 = 6;
/// The period near the end of decision or confirmation that Glove must mix and submit votes.
const GLOVE_MIX_PERIOD: u32 = (15 * 60) / BLOCK_TIME_SECS;

pub async fn calculate_mixing_time(
poll_status: ReferendumStatus,
network: &SubstrateNetwork
) -> Result<MixingTime, subxt::Error> {
let Some(deciding_status) = poll_status.deciding else {
return Ok(MixingTime::NotDeciding);
};
let decision_period = network.get_tracks()?
.get(&poll_status.track)
.map(|track_info| track_info.decision_period)
.ok_or_else(|| subxt::Error::Other("Track not found".into()))?;
let decision_end = deciding_status.since + decision_period;
if let Some(confirming_end) = deciding_status.confirming {
if confirming_end < decision_end {
return Ok(MixingTime::Confirming(confirming_end - GLOVE_MIX_PERIOD));
}
}
Ok(MixingTime::Deciding(decision_end - GLOVE_MIX_PERIOD))
}

pub enum MixingTime {
Deciding(u32),
Confirming(u32),
NotDeciding
}

impl MixingTime {
pub fn block_number(&self) -> Option<u32> {
match self {
MixingTime::Deciding(block_number) => Some(*block_number),
MixingTime::Confirming(block_number) => Some(*block_number),
MixingTime::NotDeciding => None
}
}
}

/// Voter lookup for a poll.
#[derive(Clone)]
pub struct VoterLookup {
Expand Down
108 changes: 82 additions & 26 deletions service/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use aws_sdk_dynamodb::operation::delete_item::DeleteItemError;
use aws_sdk_dynamodb::operation::put_item::PutItemError;
use aws_sdk_dynamodb::operation::query::QueryError;
use aws_sdk_dynamodb::operation::scan::ScanError;
use axum::extract::{Json, State};
use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Router;
Expand All @@ -32,21 +32,19 @@ use attestation::Error::InsecureMode;
use client_interface::{CallableSubstrateNetwork, is_glove_member, ProxyError, ReferendumStatus, ServiceInfo, SignedRemoveVoteRequest, SubstrateNetwork};
use client_interface::account_to_subxt_multi_address;
use client_interface::BatchError;
use client_interface::metadata::referenda::storage::types::referendum_info_for::ReferendumInfoFor;
use client_interface::metadata::runtime_types::frame_system::pallet::Call as SystemCall;
use client_interface::metadata::runtime_types::pallet_conviction_voting::pallet::Call as ConvictionVotingCall;
use client_interface::metadata::runtime_types::pallet_conviction_voting::pallet::Error::InsufficientFunds;
use client_interface::metadata::runtime_types::pallet_conviction_voting::pallet::Error::NotVoter;
use client_interface::metadata::runtime_types::pallet_proxy::pallet::Call as ProxyCall;
use client_interface::metadata::runtime_types::pallet_proxy::pallet::Error::NotProxy;
use client_interface::metadata::runtime_types::pallet_referenda::types::DecidingStatus;
use client_interface::metadata::runtime_types::polkadot_runtime::{RuntimeCall, RuntimeError};
use client_interface::metadata::runtime_types::polkadot_runtime::RuntimeError::Proxy;
use client_interface::subscan::Subscan;
use common::{attestation, SignedGloveResult, SignedVoteRequest};
use common::attestation::{AttestationBundle, AttestationBundleLocation, GloveProofLite};
use RuntimeError::ConvictionVoting;
use service::{GloveContext, GloveState, mixing, storage, to_proxied_vote_call};
use service::{BLOCK_TIME_SECS, calculate_mixing_time, GloveContext, GloveState, mixing, MixingTime, storage, to_proxied_vote_call};
use service::dynamodb::DynamodbGloveStorage;
use service::enclave::EnclaveHandle;
use service::storage::{GloveStorage, InMemoryGloveStorage};
Expand Down Expand Up @@ -140,13 +138,6 @@ enum EnclaveMode {
Mock
}

// TODO Sign the enclave image
// TODO Permantely ban accounts which vote directly
// TODO Endpoint for poll end time and other info?

/// The period near the end of decision or confirmation that Glove must mix and submit votes.
const GLOVE_MIX_PERIOD: u32 = (15 * 60) / 6;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let filter = EnvFilter::try_new(
Expand Down Expand Up @@ -221,6 +212,7 @@ async fn main() -> anyhow::Result<()> {
.route("/info", get(info))
.route("/vote", post(vote))
.route("/remove-vote", post(remove_vote))
.route("/poll-info/:poll_index", get(poll_info))
.layer(TraceLayer::new_for_http())
.with_state(glove_context);
let listener = TcpListener::bind(args.address).await?;
Expand Down Expand Up @@ -324,10 +316,9 @@ async fn run_background_task(context: Arc<GloveContext>, subscan: Subscan) -> an
let network = &context.network;
let storage = &context.storage;
let regular_mix_enabled = context.regular_mix_enabled;
let tracks = network.get_tracks()?;

for poll_index in storage.get_poll_indices().await? {
let Some(ReferendumInfoFor::Ongoing(status)) = network.get_poll(poll_index).await? else {
let Some(status) = network.get_ongoing_poll(poll_index).await? else {
info!("Removing poll {} as it is no longer active", poll_index);
context.remove_poll(poll_index).await?;
continue;
Expand All @@ -342,10 +333,7 @@ async fn run_background_task(context: Arc<GloveContext>, subscan: Subscan) -> an
violators, it will need to be re-mixed", poll_index);
}
} else if !mix_required {
let decision_period = tracks.get(&status.track)
.map(|track_info| track_info.decision_period)
.unwrap_or(0);
if is_poll_ready_for_final_mix(poll_index, status, decision_period, network).await? {
if is_poll_ready_for_final_mix(poll_index, status, network).await? {
mix_required = true;
}
}
Expand Down Expand Up @@ -384,21 +372,22 @@ async fn check_non_glove_voters(
async fn is_poll_ready_for_final_mix(
poll_index: u32,
poll_status: ReferendumStatus,
decision_period: u32,
network: &SubstrateNetwork
) -> Result<bool, SubxtError> {
if let Some(DecidingStatus { since: deciding_since, confirming }) = poll_status.deciding {
let now = network.current_block_number().await?;
if deciding_since + decision_period >= now - GLOVE_MIX_PERIOD {
info!("Poll {} is nearing the end of its decision period and will be mixed", poll_index);
let now = network.current_block_number().await?;
match calculate_mixing_time(poll_status, network).await? {
MixingTime::Deciding(block_number) if block_number >= now => {
info!("Poll {} is nearing the end of its decision period and will be mixed",
poll_index);
return Ok(true);
}
if confirming.is_some() && confirming.unwrap() >= now - GLOVE_MIX_PERIOD {
info!("Poll {} is nearing the end of its confirmation period and will be mixed", poll_index);
MixingTime::Confirming(block_number) if block_number >= now => {
info!("Poll {} is nearing the end of its confirmation period and will be mixed",
poll_index);
return Ok(true);
}
_ => Ok(false)
}
Ok(false)
}

async fn info(context: State<Arc<GloveContext>>) -> Json<ServiceInfo> {
Expand Down Expand Up @@ -492,6 +481,38 @@ async fn remove_vote(
Ok(())
}

async fn poll_info(
State(context): State<Arc<GloveContext>>,
Path(poll_index): Path<u32>
) -> Result<Json<PollInfo>, PollInfoError> {
if context.regular_mix_enabled {
return Err(PollInfoError::RegularMixEnabled);
}
let Some(status) = context.network.get_ongoing_poll(poll_index).await? else {
return Err(PollInfoError::PollNotActive);
};
let current_block = context.network.current_block_number().await?;
let current_time = context.network.current_time().await? / 1000;
let mixing_time = calculate_mixing_time(status, &context.network).await?
.block_number()
.map(|block_number| MixingTimeJson {
block_number,
timestamp: current_time + ((block_number - current_block) * BLOCK_TIME_SECS) as u64
});
Ok(Json(PollInfo { mixing_time }))
}

#[derive(Debug, Clone, Serialize)]
struct PollInfo {
mixing_time: Option<MixingTimeJson>
}

#[derive(Debug, Clone, Serialize)]
struct MixingTimeJson {
block_number: u32,
timestamp: u64
}

async fn mix_votes(context: &GloveContext, poll_index: u32) {
loop {
match try_mix_votes(context, poll_index).await {
Expand Down Expand Up @@ -643,7 +664,7 @@ enum InternalError {
#[error("Proxy error: {0}")]
Proxy(#[from] ProxyError),
#[error("Storage error: {0}")]
Storage(#[from] storage::Error)
Storage(#[from] storage::Error),
}

impl InternalError {
Expand Down Expand Up @@ -826,3 +847,38 @@ impl From<storage::Error> for RemoveVoteError {
RemoveVoteError::Internal(error.into())
}
}

#[derive(thiserror::Error, Debug)]
enum PollInfoError {
#[error("Regular mixing is enabled")]
RegularMixEnabled,
#[error("Poll is not active")]
PollNotActive,
#[error("Internal error: {0}")]
Internal(#[from] InternalError)
}

impl IntoResponse for PollInfoError {
fn into_response(self) -> Response {
match self {
PollInfoError::RegularMixEnabled =>
(StatusCode::NOT_FOUND, "Regular mixing is enabled".to_string()).into_response(),
PollInfoError::PollNotActive => {
(
StatusCode::BAD_REQUEST,
Json(BadRequestResponse {
error: "PollNotActive".into(),
description: self.to_string()
})
).into_response()
}
PollInfoError::Internal(error) => error.into_response()
}
}
}

impl From<SubxtError> for PollInfoError {
fn from(error: SubxtError) -> Self {
PollInfoError::Internal(error.into())
}
}

0 comments on commit c2daa11

Please sign in to comment.