Skip to content

Commit

Permalink
store failed elections (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasad1 authored Sep 12, 2024
1 parent d1ea1a9 commit 51ad5c6
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 146 deletions.
115 changes: 77 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
## Polkadot staking miner monitor

This is a simple tool that monitors each election in polkadot, kusama and westend
This is a simple tool that monitors each election in polkadot-sdk-based chains and
then stores the following data related in a postgres database:
- submissions: The list of all submissions in each election, this is regarded
as successful if the solution extrinsic is accepted by the chain. The solution may
be rejected at the end of the election when it's fully verified. You need check `slashed`
together with this to know whether a solution was truly valid.
- winners: Get the winners of each round
- elections: Get the results of each election, which may be a signed solution, an unsigned solution or a failed election.
- slashed: Get the slashed accounts

The tool is based on the subxt library and is written in Rust.
Expand All @@ -17,10 +17,8 @@ The tool is based on the subxt library and is written in Rust.
- `GET /docs/openapi.yaml` - OpenAPI YAML schema
- `GET /submissions` - Get all submissions from the database in JSON format.
- `GET /submissions/{n}` - Get the `n` most recent submissions from the database in JSON format, n is a number.
- `GET /winners` - Dump all winners from the database in JSON format.
- `GET /winners/{n}` - Get the `n` most recent winners from the database in JSON format, n is a number.
- `GET /unsigned-winners` - Get all winners that was submitted by a validator (this is fail-safe mechanism when no staking miner is available).
- `GET /unsigned-winners/{n}` - Get the `n` most recent unsigned winners from the database in JSON format, n is a number.
- `GET /elections` - Dump all elections from the database in JSON format.
- `GET /elections/{n}` - Get the `n` most recent winners from the database in JSON format, n is a number.
- `GET /slashed` - Get all slashed solutions from the database in JSON format.
- `GET /slashed/{n}` - Get the `n` most recent slashed solutions from the database in JSON format, n is a number.

Expand All @@ -46,58 +44,99 @@ Open another terminal and run the following commands to use the API:
#### Get all submissions

```bash
$ curl "http://localhost:9999/submissions"
$ curl "http://localhost:9999/submissions" | jq
[
{"who":"0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d","round":79,"block":1564,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000},"success":true},
{"who":"0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d","round":80,"block":1584,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000},"success":true},
{"who":"0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d","round":81,"block":1604,"score":{"minimal_stake":340282366920938463463374607431768211455,"sum_stake":340282366920938463463374607431768211455,"sum_stake_squared":340282366920938463463374607431768211455},"success":true},
{"who":"unsigned","round":81,"block":1612,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000},"success":true}
{
"who": "0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48",
"round": 54,
"block": 1059,
"score": {
"minimal_stake": 100000000000000,
"sum_stake": 100000000000000,
"sum_stake_squared": 10000000000000000000000000000
},
"success": true
},
{
"who": "unsigned",
"round": 55,
"block": 1087,
"score": {
"minimal_stake": 100000000000000,
"sum_stake": 100000000000000,
"sum_stake_squared": 10000000000000000000000000000
},
"success": true
}
]
```

#### Get the most recent submission
```bash
$ curl "http://localhost:9999/submissions/1"
[{"who":"unsigned","round":82,"block":1632,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000},"success":true}]
```

#### Get all winners

```bash
$ curl "http://localhost:9999/winners"
$ curl "http://localhost:9999/submissions/1" | jq
[
{"who":"0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d","round":80,"block":1581,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000}},
{"who":"0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d","round":81,"block":1601,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000}},
{"who":"unsigned","round":82,"block":1621,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000}}
{
"who": "unsigned",
"round": 57,
"block": 1127,
"score": {
"minimal_stake": 100000000000000,
"sum_stake": 100000000000000,
"sum_stake_squared": 10000000000000000000000000000
},
"success": true
}
]
```

#### Get the most recent winner
#### Get all elections

```bash
$ curl "http://localhost:9999/winners/1"
$ curl "http://localhost:9999/elections" | jq
[
{"who":"unsigned","round":82,"block":1621,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000}}
{
"result": "signed",
"who": "0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48",
"round": 55,
"block": 1076,
"score": {
"minimal_stake": 100000000000000,
"sum_stake": 100000000000000,
"sum_stake_squared": 10000000000000000000000000000
}
},
{
"result": "unsigned",
"who": null,
"round": 56,
"block": 1096,
"score": {
"minimal_stake": 100000000000000,
"sum_stake": 100000000000000,
"sum_stake_squared": 10000000000000000000000000000
}
}
]
```

#### Get all unsigned winners
#### Get the most recent election

```bash
$ curl "http://localhost:9999/unsigned-winners"
$ curl "http://localhost:9999/elections/1"
[
{"who":"unsigned","round":82,"block":1621,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000}},
{"who":"unsigned","round":83,"block":1641,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000}}
{
"result": "unsigned",
"who": null,
"round": 57,
"block": 1116,
"score": {
"minimal_stake": 100000000000000,
"sum_stake": 100000000000000,
"sum_stake_squared": 10000000000000000000000000000
}
}
]
```

#### Get the most recent unsigned winner

```bash
$ curl "http://localhost:9999/unsigned-winners/1"
[
{"who":"unsigned","round":83,"block":1641,"score":{"minimal_stake":100000000000000,"sum_stake":100000000000000,"sum_stake_squared":10000000000000000000000000000}}
]
```

#### Get all slashed solutions
Expand All @@ -120,5 +159,5 @@ $ curl "http://localhost:9999/slashed/1"

### Database migrations

This tool has a simple database with three tables: `submissions`, `election_winners` and `slashed` which is located in the `migrations` folder.
This tool has a simple database with three tables: `submissions`, `elections` and `slashed` which is located in the `migrations` folder.
To add a new migration, just create a new file with the following format: `V{version}__{description}.sql` and it will be automatically applied when the tool is started.
5 changes: 3 additions & 2 deletions migrations/V1__initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ CREATE TABLE IF NOT EXISTS submissions (
success BOOLEAN
);

CREATE TABLE IF NOT EXISTS election_winners
CREATE TABLE IF NOT EXISTS elections
(
id SERIAL PRIMARY KEY,
address TEXT,
result TEXT,
address JSONB,
round OID,
block OID,
score JSONB
Expand Down
96 changes: 43 additions & 53 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.

use crate::types::ElectionResult as InnerElectionResult;
use crate::{Address, LOG_TARGET};
use oasgen::OaSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sp_npos_elections::ElectionScore;
use std::num::NonZeroUsize;
use std::str::FromStr;
Expand Down Expand Up @@ -63,24 +65,23 @@ impl Database {
Ok(())
}

pub async fn insert_election_winner(&self, winner: Winner) -> Result<(), Error> {
let Winner {
pub async fn insert_election(&self, election: Election) -> Result<(), Error> {
let Election {
result,
who,
round,
block,
score,
} = winner;

let who = who.to_string();
} = election;

let stmt = self
.0
.prepare(
"INSERT INTO election_winners (address, round, block, score) VALUES ($1, $2, $3, $4)",
"INSERT INTO elections (result, address, round, block, score) VALUES ($1, $2, $3, $4, $5)",
)
.await?;
self.0
.execute(&stmt, &[&who, &round, &block, &score])
.execute(&stmt, &[&result, &who, &round, &block, &score])
.await?;

Ok(())
Expand Down Expand Up @@ -111,33 +112,8 @@ impl Database {
collect_db_rows(self.0.query("SELECT * FROM submissions", &[]).await?)
}

pub async fn get_all_election_winners(&self) -> Result<Vec<Winner>, Error> {
collect_db_rows(self.0.query("SELECT * FROM election_winners", &[]).await?)
}

pub async fn get_all_unsigned_winners(&self) -> Result<Vec<Winner>, Error> {
collect_db_rows(
self.0
.query(
"SELECT * FROM election_winners WHERE address = 'unsigned'",
&[],
)
.await?,
)
}

pub async fn get_most_recent_unsigned_winners(
&self,
n: NonZeroUsize,
) -> Result<Vec<Winner>, Error> {
collect_db_rows(
self.0
.query(
&format!("SELECT * FROM election_winners WHERE address = 'unsigned' ORDER BY round DESC LIMIT {n}"),
&[],
)
.await?,
)
pub async fn get_all_elections(&self) -> Result<Vec<Election>, Error> {
collect_db_rows(self.0.query("SELECT * FROM elections", &[]).await?)
}

pub async fn get_most_recent_submissions(
Expand All @@ -154,14 +130,11 @@ impl Database {
)
}

pub async fn get_most_recent_election_winners(
&self,
n: NonZeroUsize,
) -> Result<Vec<Winner>, Error> {
pub async fn get_most_recent_elections(&self, n: NonZeroUsize) -> Result<Vec<Election>, Error> {
collect_db_rows(
self.0
.query(
&format!("SELECT * FROM election_winners ORDER BY round DESC LIMIT {n}"),
&format!("SELECT * FROM elections ORDER BY round DESC LIMIT {n}"),
&[],
)
.await?,
Expand Down Expand Up @@ -247,16 +220,32 @@ impl TryFrom<Row> for Submission {
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, OaSchema)]
pub struct Winner {
who: Address,
pub struct Election {
result: String,
who: serde_json::Value,
round: u32,
block: u32,
score: serde_json::Value,
}

impl Winner {
pub fn new(who: Address, round: u32, block: u32, score: ElectionScore) -> Self {
impl Election {
pub fn new(
election: InnerElectionResult,
round: u32,
block: u32,
score: ElectionScore,
) -> Self {
let (result, who) = match election {
InnerElectionResult::Signed(addr) => (
"signed".to_string(),
serde_json::to_value(&addr).expect("AccountId serialize infallible; qed"),
),
InnerElectionResult::Unsigned => ("unsigned".to_string(), json!(null)),
InnerElectionResult::Failed => ("election failed".to_string(), json!(null)),
};

Self {
result,
who,
round,
block,
Expand All @@ -265,21 +254,22 @@ impl Winner {
}
}

impl TryFrom<Row> for Winner {
impl TryFrom<Row> for Election {
type Error = Error;

fn try_from(row: Row) -> Result<Self, Self::Error> {
let who = {
let val: String = row
.try_get(1)
.map_err(|_| Error::RowNotFound("address", 1))?;
Address::from_str(&val).map_err(|e| Error::Parse(e.to_string()))?
};
let round = row.try_get(2).map_err(|_| Error::RowNotFound("round", 2))?;
let block = row.try_get(3).map_err(|_| Error::RowNotFound("block", 3))?;
let score = row.try_get(4).map_err(|_| Error::RowNotFound("score", 4))?;
let result = row
.try_get(1)
.map_err(|_| Error::RowNotFound("result", 1))?;
let who: serde_json::Value = row
.try_get(2)
.map_err(|_| Error::RowNotFound("address", 2))?;
let round = row.try_get(3).map_err(|_| Error::RowNotFound("round", 3))?;
let block = row.try_get(4).map_err(|_| Error::RowNotFound("block", 4))?;
let score = row.try_get(5).map_err(|_| Error::RowNotFound("score", 5))?;

Ok(Self {
result,
who,
round,
block,
Expand Down
7 changes: 7 additions & 0 deletions src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ pub async fn read_block(
))
.await?;
}

if event
.as_event::<runtime::election_provider_multi_phase::events::ElectionFailed>()?
.is_some()
{
state.election_failed();
}
}

for (_, missed) in submissions.into_iter() {
Expand Down
Loading

0 comments on commit 51ad5c6

Please sign in to comment.