Skip to content

Commit

Permalink
OCV - Consideration phase (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
leomanza authored Dec 3, 2024
1 parent 2391292 commit 8f03c00
Show file tree
Hide file tree
Showing 13 changed files with 329 additions and 22 deletions.
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,75 @@ For more details, see:
- The [article by Granola](https://granola.team/blog/mina-on-chain-voting-results-instructions/)
- The [FAQ](https://forums.minaprotocol.com/t/on-chain-voting-frequently-asked-questions-faq/5959)


## Voting on a MEF

To cast a vote on a particular MEF, a user must send a transaction to **themselves** with a
specially-constructed memo field. The memo field must adhere to the following convention.

**For example:**

```
To vote in favor of 'MEF 1', the memo field must be populated with: 'YES ID 1'
Similarly, if the intent is to vote against 'MEF 1', the memo field must
contain: 'NO ID 1'.
```

*Vote With Auro Wallet*
- Ensure you're on the correct network (e.g., devnet).
- Click on your wallet address to copy it to the clipboard (you’ll need it in step 4).
- Navigate to the Send feature in the wallet.
- In the To field, paste your own wallet address.
- Enter 1 in the Amount field.
- To cast your vote:
- Enter YES ID # in the Memo field to vote in favor of the proposal.
- Enter NO ID # in the Memo field to vote against the proposal.
- Replace # with the actual proposal ID you are voting for.
- Confirm and submit the transaction. Your vote will be recorded on-chain.

# Proposal Consideration API

## Overview
The **Proposal Consideration API** allows users to fetch detailed data about a proposal, including community votes, staking weights, and vote details within a specified time range.

---

## Endpoint

### `GET /api/mef_proposal_consideration/:id/:start_time/:end_time?ledger_hash`

Retrieve details for a specific proposal within a specified time frame. Optionally, you can configure the ledger hash to compute vote weights.

### Path Parameters
| Parameter | Type | Description |
|--------------|-----------|------------------------------------------------------|
| `id` | `integer` | Unique identifier for the proposal. |
| `start_time` | `integer` | Proposal start time in milliseconds unix timestamp. |
| `end_time` | `integer` | Proposal end time in milliseconds unix timestamp. |

### Query Parameters (optional)
| Parameter | Type | Description |
|--------------|-----------|------------------------------------------------------|
| `ledger_hash` | `string` | Ledger hash used to compute weights of the votes. |

---

### Response Details

| Field | Description |
|-------------------------|----------------------------------------------------------------|
| `proposal_id` | Unique identifier of the proposal. |
| `total_community_votes` | Total number of votes cast by the community. |
| `total_positive_community_votes` | Total number of positive votes cast by the community. |
| `total_negative_community_votes` | Total number of negative votes cast by the community. |
| `total_stake_weight` | Total staking weight applied to the proposal. |
| `positive_stake_weight` | Staking weight supporting the proposal. |
| `negative_stake_weight` | Staking weight opposing the proposal. |
| `vote_status` | Current status of the proposal. |
| `elegible` | Elegible status |

---

## Software Development

Install [Nix](https://nixos.org/download) and [direnv](https://direnv.net/docs/installation.html).
Expand Down
2 changes: 1 addition & 1 deletion server/Cargo.lock

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

2 changes: 1 addition & 1 deletion server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mina-ocv"
version = "0.11.0"
version = "0.12.0"
edition = "2021"

[dependencies]
Expand Down
2 changes: 1 addition & 1 deletion server/proposals/proposals.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,4 @@
"network": "devnet"
}
]
}
}
2 changes: 1 addition & 1 deletion server/proposals/proposals_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,4 @@
}
},
"required": ["proposals"]
}
}
5 changes: 3 additions & 2 deletions server/src/archive.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::{BlockStatus, ChainStatusType};
use anyhow::{Context, Result};
use diesel::{
PgConnection, QueryableByName, RunQueryDsl,
r2d2::ConnectionManager,
sql_query,
sql_types::{BigInt, Text},
PgConnection, QueryableByName, RunQueryDsl,
};
use r2d2::Pool;

use crate::{BlockStatus, ChainStatusType};

#[derive(Clone)]
pub struct Archive(Pool<ConnectionManager<PgConnection>>);

Expand Down
6 changes: 4 additions & 2 deletions server/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::{Archive, Caches, Ocv, Proposal, ProposalsManifest};
use std::{fs, path::PathBuf, str::FromStr};

use anyhow::Result;
use bytes::Bytes;
use clap::{Args, Parser, ValueEnum};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf, str::FromStr};

use crate::{Archive, Caches, Ocv, Proposal, ProposalsManifest};

#[derive(Clone, Args)]
pub struct OcvConfig {
Expand Down
41 changes: 38 additions & 3 deletions server/src/ledger.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use crate::{s3_client, Ocv, ProposalVersion, Vote, Wrapper};
use anyhow::{anyhow, Result};
use std::{collections::HashMap, fs, io::Read, path::PathBuf};

use anyhow::{Result, anyhow};
use flate2::read::GzDecoder;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, io::Read, path::PathBuf};
use tar::Archive;

use crate::{Ocv, ProposalVersion, Vote, Wrapper, s3_client};

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct Ledger(pub Vec<LedgerAccount>);

Expand Down Expand Up @@ -107,6 +109,39 @@ impl Ledger {
}
}
}

pub fn get_stake_weight_mep(
&self,
_map: &Wrapper<HashMap<String, Vote>>,
public_key: impl Into<String>,
) -> Result<Decimal> {
let public_key: String = public_key.into();

let account =
self.0.iter().find(|d| d.pk == public_key).ok_or_else(|| anyhow!("account {public_key} not found in ledger"))?;

let balance = account.balance.parse().unwrap_or_else(|_| Decimal::new(0, LEDGER_BALANCE_SCALE));

if account.delegate.clone().unwrap_or(public_key.clone()) != public_key {
return Ok(Decimal::new(0, LEDGER_BALANCE_SCALE));
}

let delegators = self
.0
.iter()
.filter(|d| d.delegate.clone().unwrap_or(d.pk.clone()) == public_key && d.pk != public_key)
.collect::<Vec<&LedgerAccount>>();

if delegators.is_empty() {
return Ok(balance);
}

let stake_weight = delegators.iter().fold(Decimal::new(0, LEDGER_BALANCE_SCALE), |acc, x| {
x.balance.parse().unwrap_or_else(|_| Decimal::new(0, LEDGER_BALANCE_SCALE)) + acc
});

Ok(stake_weight + balance)
}
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
Expand Down
136 changes: 133 additions & 3 deletions server/src/ocv.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::{util::Caches, Archive, Ledger, Network, Proposal, Vote, VoteWithWeight, Wrapper};
use anyhow::{anyhow, Result};
use std::{path::PathBuf, sync::Arc};

use anyhow::{Result, anyhow};
use rust_decimal::Decimal;
use serde::Serialize;
use std::{path::PathBuf, sync::Arc};

use crate::{Archive, Ledger, Network, Proposal, Vote, VoteWithWeight, Wrapper, util::Caches};

#[derive(Clone)]
pub struct Ocv {
Expand Down Expand Up @@ -43,6 +45,120 @@ impl Ocv {
Ok(ProposalResponse { proposal, votes })
}

pub async fn proposal_consideration(
&self,
id: usize,
start_time: i64,
end_time: i64,
ledger_hash: Option<String>,
) -> Result<GetMinaProposalConsiderationResponse> {
let proposal_key = "MEF".to_string() + &id.to_string();
let votes = if let Some(cached_votes) = self.caches.votes.get(&proposal_key).await {
cached_votes.to_vec()
} else {
let transactions = self.archive.fetch_transactions(start_time, end_time)?;

let chain_tip = self.archive.fetch_chain_tip()?;
let votes = Wrapper(transactions.into_iter().map(std::convert::Into::into).collect())
.process_mep(id, chain_tip)
.sort_by_timestamp()
.to_vec()
.0;

self.caches.votes.insert(proposal_key.clone(), Arc::new(votes.clone())).await;
tracing::info!("votes {}", votes.len());
votes
};

// weighted votes
let mut positive_stake_weight = Decimal::from(0);
let mut negative_stake_weight = Decimal::from(0);

// check community votes
let mut total_positive_community_votes = 0;
let mut total_negative_community_votes = 0;
for vote in &votes {
if vote.memo.to_lowercase() == format!("yes id {}", id) {
total_positive_community_votes += 1;
}
if vote.memo.to_lowercase() == format!("no id {}", id) {
total_negative_community_votes += 1;
}
}
// Check if enough positive votes
if total_positive_community_votes < 10 {
return Ok(GetMinaProposalConsiderationResponse {
proposal_id: id,
total_community_votes: votes.len(),
total_positive_community_votes,
total_negative_community_votes,
total_stake_weight: Decimal::ZERO,
positive_stake_weight: Decimal::ZERO,
negative_stake_weight: Decimal::ZERO,
votes,
elegible: false,
vote_status: "Insufficient voters".to_string(),
});
}

// Calculate weighted votes if ledger_hash params is provided
if let Some(hash) = ledger_hash {
let votes_weighted = if let Some(cached_votes) = self.caches.votes_weighted.get(&proposal_key).await {
cached_votes.to_vec()
} else {
let transactions = self.archive.fetch_transactions(start_time, end_time)?;

let chain_tip = self.archive.fetch_chain_tip()?;

let ledger = if let Some(cached_ledger) = self.caches.ledger.get(&hash).await {
Ledger(cached_ledger.to_vec())
} else {
let ledger = Ledger::fetch(self, &hash).await?;

self.caches.ledger.insert(hash, Arc::new(ledger.0.clone())).await;

ledger
};

let votes = Wrapper(transactions.into_iter().map(std::convert::Into::into).collect())
.into_weighted_mep(id, &ledger, chain_tip)
.sort_by_timestamp()
.0;

self.caches.votes_weighted.insert(proposal_key.clone(), Arc::new(votes.clone())).await;

votes
};
for vote in &votes_weighted {
if vote.memo.to_lowercase() == format!("no id {}", id) {
negative_stake_weight += vote.weight;
}
if vote.memo.to_lowercase() == format!("yes id {}", id) {
positive_stake_weight += vote.weight;
}
positive_stake_weight += vote.weight;
}
} else {
tracing::info!("ledger_hash is not provided.");
}

let total_stake_weight = positive_stake_weight + negative_stake_weight;

// Voting results
Ok(GetMinaProposalConsiderationResponse {
proposal_id: id,
total_community_votes: votes.len(),
total_positive_community_votes,
total_negative_community_votes,
total_stake_weight,
positive_stake_weight,
negative_stake_weight,
votes,
elegible: true,
vote_status: "Proposal selected for the next phase".to_string(),
})
}

pub async fn proposal_result(&self, id: usize) -> Result<GetMinaProposalResultResponse> {
let proposal = self.find_proposal(id)?;
let hash = match proposal.ledger_hash.clone() {
Expand Down Expand Up @@ -132,3 +248,17 @@ pub struct GetMinaProposalResultResponse {
negative_stake_weight: Decimal,
votes: Vec<VoteWithWeight>,
}

#[derive(Serialize)]
pub struct GetMinaProposalConsiderationResponse {
proposal_id: usize,
total_community_votes: usize,
total_positive_community_votes: usize,
total_negative_community_votes: usize,
total_stake_weight: Decimal,
positive_stake_weight: Decimal,
negative_stake_weight: Decimal,
votes: Vec<Vote>,
vote_status: String,
elegible: bool,
}
16 changes: 14 additions & 2 deletions server/src/serve.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};

use anyhow::Result;
use axum::{
Json, Router, debug_handler,
extract::{Path, State},
extract::{Path, Query, State},
response::IntoResponse,
routing::get,
serve as axum_serve,
Expand Down Expand Up @@ -40,6 +40,7 @@ impl ServeArgs {
.route("/api/proposals", get(get_proposals))
.route("/api/proposal/:id", get(get_proposal))
.route("/api/proposal/:id/results", get(get_proposal_result))
.route("/api/mef_proposal_consideration/:id/:start_time/:end_time", get(get_proposal_consideration))
.layer(CorsLayer::permissive())
.with_state(Arc::new(ocv));
axum_serve(listener, router).with_graceful_shutdown(shutdown_signal()).await?;
Expand Down Expand Up @@ -70,3 +71,14 @@ async fn get_proposal_result(ctx: State<Arc<Ocv>>, Path(id): Path<usize>) -> imp
tracing::info!("get_proposal_result {}", id);
Wrapper(ctx.proposal_result(id).await)
}

#[debug_handler]
async fn get_proposal_consideration(
ctx: State<Arc<Ocv>>,
Path((id, start_time, end_time)): Path<(usize, i64, i64)>,
Query(params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
let ledger_hash = params.get("ledger_hash").cloned();
tracing::info!("get_proposal_consideration {} {} {}", id, start_time, end_time);
Wrapper(ctx.proposal_consideration(id, start_time, end_time, ledger_hash).await)
}
6 changes: 4 additions & 2 deletions server/src/util/caches.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::{ledger::LedgerAccount, Vote, VoteWithWeight};
use moka::future::Cache as MokaCache;
use std::sync::Arc;

use moka::future::Cache as MokaCache;

use crate::{Vote, VoteWithWeight, ledger::LedgerAccount};

#[derive(Clone)]
pub struct Caches {
pub votes: MokaCache<String, Arc<Vec<Vote>>>,
Expand Down
Loading

0 comments on commit 8f03c00

Please sign in to comment.