Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API V2: status endpoint #245

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use crate::api::v2;
use crate::{
api::v1::{self},
rpc::Node,
types::{RuntimeConfig, State},
};
use anyhow::Context;
Expand All @@ -31,6 +32,7 @@ pub struct Server {
pub state: Arc<Mutex<State>>,
pub version: String,
pub network_version: String,
pub node: Node,
}

impl Server {
Expand All @@ -41,15 +43,21 @@ impl Server {
http_server_port: port,
app_id,
..
} = self.cfg;
} = self.cfg.clone();

let port = (port.1 > 0)
.then(|| thread_rng().gen_range(port.0..=port.1))
.unwrap_or(port.0);

let v1_api = v1::routes(self.db.clone(), app_id, self.state.clone());
#[cfg(feature = "api-v2")]
let v2_api = v2::routes(self.version.clone(), self.network_version.clone());
let v2_api = v2::routes(
self.version.clone(),
self.network_version.clone(),
self.node,
self.state.clone(),
self.cfg,
);

let cors = warp::cors()
.allow_any_origin()
Expand Down
71 changes: 71 additions & 0 deletions src/api/v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,77 @@ Content-Type: application/json
- **version** - the Avail Light Client version
- **network_version** - Avail network version supported by the Avail Light Client

## **GET** `/v2/status`

Gets current status and active modes of the light client.

Response:

```yaml
HTTP/1.1 200 OK
Content-Type: application/json

{
"modes": [
"light",
"app",
"partition"
],
"app_id": {app-id}, // Optional
"genesis_hash": "{genesis-hash}",
"network": "{network}",
"blocks": {
"latest": {latest},
"available": { // Optional
"first": {first},
"last": {last}
},
"app_data": { // Optional
"first": {first},
"last": {last}
},
"historcal_sync": { // Optional
"synced": false,
"available": { // Optional
"first": {first},
"last": {last}
},
"app_data": { // Optional
"first": {first},
"last": {last}
}
}
},
"partition": "{partition}" // Optional
}
```

- **modes** - active modes
- **app_id** - if **app** mode is active, this field contains configured application ID
- **genesis_hash** - genesis hash of the network to which the light client is connected
- **network** - network host, version and spec version light client is currently con
- **blocks** - state of processed blocks
- **partition** - if configured, displays partition which light client distributes to the peer to peer network

### Modes

- **light** - data availability sampling mode, the light client performs random sampling and calculates confidence
- **app** - light client fetches, verifies, and stores application-related data
- **partition** - light client fetches configured block partition and publishes it to the DHT

### Blocks

- **latest** - block number of the latest [finalized](https://docs.substrate.io/learn/consensus/) block received from the node
- **available** - range of blocks with verified data availability (configured confidence has been achieved)
- **app_data** - range of blocks with app data retrieved and verified
- **historical_sync** - state for historical blocks syncing up to configured block (ommited if historical sync is not configured)

### Historical sync

- **synced** - `true` if there are no historical blocks left to sync
- **available** - range of historical blocks with verified data availability (configured confidence has been achieved)
- **app_data** - range of historical blocks with app data retrieved and verified

# WebSocket API

The Avail Light Client WebSocket API allows real-time communication between a client and a server over a persistent connection, enabling push notifications as an alternative to polling. Web socket API can be used on its own or in combination with HTTP API to enable different pull/push use cases.
Expand Down
61 changes: 59 additions & 2 deletions src/api/v2/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
use super::{
types::{Client, Clients, Subscription, SubscriptionId, Version},
types::{
BlockRange, Blocks, Client, Clients, HistoricalSync, Status, Subscription, SubscriptionId,
Version,
},
ws,
};
use std::convert::Infallible;
use crate::{
api::v2::types::InternalServerError,
rpc::Node,
types::{RuntimeConfig, State},
};
use hyper::StatusCode;
use std::{
convert::Infallible,
sync::{Arc, Mutex},
};
use tracing::info;
use uuid::Uuid;
use warp::{ws::Ws, Rejection, Reply};

Expand All @@ -28,3 +41,47 @@ pub async fn ws(
// Multiple connections to the same client are currently allowed
Ok(ws.on_upgrade(move |web_socket| ws::connect(subscription_id, web_socket, clients, version)))
}

pub async fn status(
config: RuntimeConfig,
node: Node,
state: Arc<Mutex<State>>,
) -> Result<impl Reply, impl Reply> {
let state = match state.lock() {
Ok(state) => state,
Err(error) => {
info!("Cannot acquire lock for last_block: {error}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
},
};

let historical_sync = state.synced.map(|synced| HistoricalSync {
synced,
available: state.sync_confidence_achieved.as_ref().map(From::from),
app_data: state.sync_data_verified.as_ref().map(From::from),
});

let blocks = Blocks {
latest: state.latest,
available: state.confidence_achieved.as_ref().map(From::from),
app_data: state.data_verified.as_ref().map(From::from),
historical_sync,
};

let status = Status {
modes: (&config).into(),
app_id: config.app_id,
genesis_hash: format!("{:?}", node.genesis_hash),
network: node.network(),
blocks,
partition: config.block_matrix_partition,
};
Ok(status)
}

pub async fn handle_rejection(error: Rejection) -> Result<impl Reply, Rejection> {
if error.find::<InternalServerError>().is_some() {
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
}
Err(error)
}
117 changes: 110 additions & 7 deletions src/api/v2/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
use self::types::{Clients, Version};
use std::{collections::HashMap, convert::Infallible, sync::Arc};
use crate::{
rpc::Node,
types::{RuntimeConfig, State},
};

use self::{
handlers::handle_rejection,
types::{Clients, Version},
};
use std::{
collections::HashMap,
convert::Infallible,
sync::{Arc, Mutex},
};
use tokio::sync::RwLock;
use warp::{Filter, Rejection, Reply};

Expand All @@ -19,6 +31,20 @@ fn version_route(
.map(move || version.clone())
}

fn status_route(
config: RuntimeConfig,
node: Node,
state: Arc<Mutex<State>>,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
warp::path!("v2" / "status")
.and(warp::get())
.and(warp::any().map(move || config.clone()))
.and(warp::any().map(move || node.clone()))
.and(warp::any().map(move || state.clone()))
.then(handlers::status)
.map(types::handle_result)
}

fn subscriptions_route(
clients: Clients,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
Expand All @@ -43,39 +69,50 @@ fn ws_route(
pub fn routes(
version: String,
network_version: String,
node: Node,
state: Arc<Mutex<State>>,
config: RuntimeConfig,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
let clients: Clients = Arc::new(RwLock::new(HashMap::new()));
let version = Version {
version,
network_version,
};
version_route(version.clone())
.or(status_route(config, node, state))
.or(subscriptions_route(clients.clone()))
.or(ws_route(clients, version))
.recover(handle_rejection)
}

#[cfg(test)]
mod tests {
use crate::api::v2::types::{
Clients, DataFields, Subscription, SubscriptionId, Topics, Version,
use super::types::Client;
use crate::{
api::v2::types::{Clients, DataFields, Subscription, SubscriptionId, Topics, Version},
rpc::Node,
types::{RuntimeConfig, State},
};
use kate_recovery::matrix::Partition;
use sp_core::H256;
use std::{
collections::{HashMap, HashSet},
str::FromStr,
sync::Arc,
sync::{Arc, Mutex},
};
use tokio::sync::RwLock;
use uuid::Uuid;

use super::types::Client;

fn v1() -> Version {
Version {
version: "v1.0.0".to_string(),
network_version: "nv1.0.0".to_string(),
}
}

const GENESIS_HASH: &str = "0xc590b3c924c35c2f241746522284e4709df490d73a38aaa7d6de4ed1eac2f500";
const NETWORK: &str = "{host}/{system_version}/data-avail/0";

#[tokio::test]
async fn version_route() {
let route = super::version_route(v1());
Expand All @@ -91,6 +128,72 @@ mod tests {
);
}

impl Default for Node {
fn default() -> Self {
Self {
host: "{host}".to_string(),
system_version: "{system_version}".to_string(),
spec_version: 0,
genesis_hash: H256::from_str(GENESIS_HASH).unwrap(),
}
}
}

#[tokio::test]
async fn status_route_defaults() {
let state = Arc::new(Mutex::new(State::default()));
let route = super::status_route(RuntimeConfig::default(), Node::default(), state);
let response = warp::test::request()
.method("GET")
.path("/v2/status")
.reply(&route)
.await;

let expected = format!(
r#"{{"modes":["light"],"genesis_hash":"{GENESIS_HASH}","network":"{NETWORK}","blocks":{{"latest":0}}}}"#
);
assert_eq!(response.body(), &expected);
}

#[tokio::test]
async fn status_route() {
let runtime_config = RuntimeConfig {
app_id: Some(1),
sync_start_block: Some(10),
block_matrix_partition: Some(Partition {
number: 1,
fraction: 10,
}),
..Default::default()
};
let state = Arc::new(Mutex::new(State::default()));
{
let mut state = state.lock().unwrap();
state.latest = 30;
state.set_confidence_achieved(20);
state.set_confidence_achieved(29);
state.set_data_verified(20);
state.set_data_verified(29);
state.set_synced(false);
state.set_sync_confidence_achieved(10);
state.set_sync_confidence_achieved(19);
state.set_sync_data_verified(10);
state.set_sync_data_verified(18);
}

let route = super::status_route(runtime_config, Node::default(), state);
let response = warp::test::request()
.method("GET")
.path("/v2/status")
.reply(&route)
.await;

let expected = format!(
r#"{{"modes":["light","app","partition"],"app_id":1,"genesis_hash":"{GENESIS_HASH}","network":"{NETWORK}","blocks":{{"latest":30,"available":{{"first":20,"last":29}},"app_data":{{"first":20,"last":29}},"historical_sync":{{"synced":false,"available":{{"first":10,"last":19}},"app_data":{{"first":10,"last":18}}}}}},"partition":"1/10"}}"#
);
assert_eq!(response.body(), &expected);
}

fn all_topics() -> HashSet<Topics> {
vec![
Topics::HeaderVerified,
Expand Down
Loading
Loading