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

Root event date #125

Merged
merged 23 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fa801d4
Add function to convert a block height to unix timestamp
thobson88 Sep 15, 2023
be9b628
Implement date-to-block-height-range function
thobson88 Sep 16, 2023
a99ff5a
Remove client argument from query_mongodb function
thobson88 Sep 17, 2023
dfc3242
Fix unit tests; add new mongo DB query function
thobson88 Sep 18, 2023
0550963
Implement function to query MongoDB over a block height interval
thobson88 Sep 18, 2023
18ee41b
Add root module; refactor utils functions
thobson88 Sep 22, 2023
0e46a98
Add opIndex=0 condition for root DID candidates
thobson88 Sep 24, 2023
ccb433f
Add HTTP endpoint for getting root DID candidates
thobson88 Sep 24, 2023
61a3d2d
Add RootCandidatesResult type; add assertions in unit test
thobson88 Sep 25, 2023
f48b4c7
Remove obsolete comment
thobson88 Sep 25, 2023
23c66ca
Add cache of root DID candidates in AppState
thobson88 Sep 25, 2023
bc931a3
Add date field in RootCandidatesResult struct
thobson88 Sep 26, 2023
1fb6333
Rename txid field in RootCandidate struct
thobson88 Sep 28, 2023
fad5cbc
Add HTTP endpoint to get block timestamp
thobson88 Sep 28, 2023
77b5dbc
Add block height field in RootCandidate struct
thobson88 Sep 28, 2023
9997e0b
Merge branch 'main' into root-event-date
thobson88 Oct 11, 2023
6326fec
Fix build errors
thobson88 Oct 11, 2023
cb106e5
Clippy fixes
thobson88 Oct 13, 2023
22a03a8
Use RwLock for root candidates instead of Mutex
thobson88 Oct 13, 2023
717c9b5
Delete obsolete comment
thobson88 Oct 13, 2023
92fafb0
Clippy fixes
thobson88 Oct 13, 2023
e5bc179
Remove obsolete comments
thobson88 Oct 13, 2023
07ab3c3
Improve HTTP status codes on root error
thobson88 Oct 13, 2023
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
2 changes: 1 addition & 1 deletion trustchain-api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ mod tests {
use trustchain_ion::verifier::IONVerifier;

// The root event time of DID documents in `trustchain-ion/src/data.rs` used for unit tests and the test below.
const ROOT_EVENT_TIME_1: u32 = 1666265405;
const ROOT_EVENT_TIME_1: u64 = 1666265405;

const TEST_UNSIGNED_VC: &str = r##"{
"@context": [
Expand Down
16 changes: 9 additions & 7 deletions trustchain-cli/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@
Some(time) => time.parse::<u32>().unwrap(),
None => cli_config().root_event_time,
};
let did_chain = TrustchainAPI::verify(did, root_event_time, &verifier).await?;
let did_chain =
TrustchainAPI::verify(did, root_event_time.into(), &verifier).await?;
println!("{did_chain}");
}
_ => panic!("Unrecognised DID subcommand."),
Expand Down Expand Up @@ -186,8 +187,8 @@
Some(("verify", sub_matches)) => {
let verbose = sub_matches.get_one::<u8>("verbose");
let root_event_time = match sub_matches.get_one::<String>("root_event_time") {
Some(time) => time.parse::<u32>().unwrap(),
None => cli_config().root_event_time,
Some(time) => time.parse::<u64>().unwrap(),
None => cli_config().root_event_time.into(),
};
// Deserialize
let credential: Credential =
Expand Down Expand Up @@ -238,10 +239,11 @@
let issuer = credential
.get_issuer()
.expect("No issuer present in credential.");
let chain = TrustchainAPI::verify(issuer, root_event_time, &verifier)
.await
// Can unwrap as already verified above.
.unwrap();
let chain =
TrustchainAPI::verify(issuer, root_event_time.into(), &verifier)

Check warning on line 243 in trustchain-cli/src/bin/main.rs

View workflow job for this annotation

GitHub Actions / clippy

useless conversion to the same type: `u64`

warning: useless conversion to the same type: `u64` --> trustchain-cli/src/bin/main.rs:243:59 | 243 | ... TrustchainAPI::verify(issuer, root_event_time.into(), &verifier) | ^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `.into()`: `root_event_time` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion = note: `#[warn(clippy::useless_conversion)]` on by default
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove into() (clippy)

.await
// Can unwrap as already verified above.
.unwrap();
if verbose_count > 1 {
let (_, doc, doc_meta) =
resolver.resolve_as_result(issuer).await.unwrap();
Expand Down
10 changes: 10 additions & 0 deletions trustchain-core/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use ssi::jwk::JWK;
use ssi::one_or_many::OneOrMany;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Once;

/// Gets the type of an object as a String. For diagnostic purposes (debugging) only.
Expand Down Expand Up @@ -99,6 +100,15 @@
did.split(':').last().unwrap()
}

/// Converts a short-form DID into a complete DID.
pub fn get_did_from_suffix(did_suffix: &str, method: &str) -> String {
thobson88 marked this conversation as resolved.
Show resolved Hide resolved
sgreenbury marked this conversation as resolved.
Show resolved Hide resolved
let mut did = String::from_str("did:").unwrap();
did.push_str(method);
did.push_str(":");

Check warning on line 107 in trustchain-core/src/utils.rs

View workflow job for this annotation

GitHub Actions / clippy

calling `push_str()` using a single-character string literal

warning: calling `push_str()` using a single-character string literal --> trustchain-core/src/utils.rs:107:5 | 107 | did.push_str(":"); | ^^^^^^^^^^^^^^^^^ help: consider using `push` with a character literal: `did.push(':')` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_add_str = note: `#[warn(clippy::single_char_add_str)]` on by default
did.push_str(did_suffix);
did
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let mut did = String::from_str("did:").unwrap();
did.push_str(method);
did.push_str(":");
did.push_str(did_suffix);
did
format!("did:{method}:{did_suffix}")

}

/// [`JSON_CANONICALIZATION_SCHEME`](https://identity.foundation/sidetree/spec/v1.0.0/#json-canonicalization-scheme)
pub fn canonicalize<T: Serialize + ?Sized>(value: &T) -> Result<String, serde_json::Error> {
serde_jcs::to_string(value)
Expand Down
2 changes: 1 addition & 1 deletion trustchain-core/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ impl From<serde_json::Error> for VerifierError {
}

/// A Unix timestamp.
pub type Timestamp = u32;
pub type Timestamp = u64;

/// A verifiably-timestamped DID.
pub trait VerifiableTimestamp {
Expand Down
4 changes: 2 additions & 2 deletions trustchain-ffi/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use ssi::vc::LinkedDataProofOptions;
use std::{fs, str::FromStr};
use trustchain_core::TRUSTCHAIN_CONFIG;
use trustchain_core::{verifier::Timestamp, TRUSTCHAIN_CONFIG};
use trustchain_ion::{Endpoint, URL};

use crate::mobile::FFIMobileError;
Expand Down Expand Up @@ -65,7 +65,7 @@ impl Default for EndpointOptions {
#[serde(rename_all = "camelCase")]
pub struct TrustchainOptions {
pub signature_only: bool,
pub root_event_time: u32,
pub root_event_time: Timestamp,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
Expand Down
11 changes: 11 additions & 0 deletions trustchain-http/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use trustchain_core::{
commitment::CommitmentError, issuer::IssuerError, resolver::ResolverError, vc::CredentialError,
verifier::VerifierError, vp::PresentationError,
};
use trustchain_ion::root::TrustchainRootError;

// TODO: refine and add doc comments for error variants
#[derive(Error, Debug)]
Expand All @@ -20,6 +21,8 @@ pub enum TrustchainHTTPError {
ResolverError(ResolverError),
#[error("Trustchain issuer error: {0}")]
IssuerError(IssuerError),
#[error("Trustchain root error: {0}")]
RootError(TrustchainRootError),
#[error("Trustchain presentation error: {0}")]
PresentationError(PresentationError),
#[error("Credential does not exist.")]
Expand Down Expand Up @@ -55,12 +58,19 @@ impl From<VerifierError> for TrustchainHTTPError {
TrustchainHTTPError::VerifierError(err)
}
}

impl From<IssuerError> for TrustchainHTTPError {
fn from(err: IssuerError) -> Self {
TrustchainHTTPError::IssuerError(err)
}
}

impl From<TrustchainRootError> for TrustchainHTTPError {
fn from(err: TrustchainRootError) -> Self {
TrustchainHTTPError::RootError(err)
}
}

impl From<PresentationError> for TrustchainHTTPError {
fn from(err: PresentationError) -> Self {
TrustchainHTTPError::PresentationError(err)
Expand Down Expand Up @@ -108,6 +118,7 @@ impl IntoResponse for TrustchainHTTPError {
err @ TrustchainHTTPError::NoCredentialIssuer => {
(StatusCode::BAD_REQUEST, err.to_string())
}
err @ TrustchainHTTPError::RootError(_) => (StatusCode::BAD_REQUEST, err.to_string()),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the HTTP status

err @ TrustchainHTTPError::FailedToVerifyCredential => {
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
}
Expand Down
1 change: 1 addition & 0 deletions trustchain-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod issuer;
pub mod middleware;
pub mod qrcode;
pub mod resolver;
pub mod root;
pub mod server;
pub mod state;
pub mod static_handlers;
Expand Down
218 changes: 218 additions & 0 deletions trustchain-http/src/root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
use crate::state::AppState;
use async_trait::async_trait;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use chrono::NaiveDate;
use log::debug;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use trustchain_core::verifier::Timestamp;
use trustchain_ion::root::{root_did_candidates, RootCandidate, TrustchainRootError};
use trustchain_ion::utils::time_at_block_height;

use crate::errors::TrustchainHTTPError;

/// An HTTP API for identifying candidate root DIDs.
#[async_trait]
pub trait TrustchainRootHTTP {
/// Gets a vector of root DID candidates timestamped on a given date.
async fn root_candidates(
date: NaiveDate,
root_candidates: &Mutex<HashMap<NaiveDate, RootCandidatesResult>>,
) -> Result<RootCandidatesResult, TrustchainHTTPError>;
/// Gets a unix timestamp for a given Bitcoin transaction ID.
async fn block_timestamp(height: u64) -> Result<TimestampResult, TrustchainHTTPError>;
}

/// Type for implementing the TrustchainIssuerHTTP trait that will contain additional handler methods.
pub struct TrustchainRootHTTPHandler {}

#[async_trait]
impl TrustchainRootHTTP for TrustchainRootHTTPHandler {
async fn root_candidates(
date: NaiveDate,
root_candidates: &Mutex<HashMap<NaiveDate, RootCandidatesResult>>,
) -> Result<RootCandidatesResult, TrustchainHTTPError> {
debug!("Getting root candidates for {0}", date);

// Return the cached vector of root DID candidates, if available.
if root_candidates.lock().unwrap().contains_key(&date) {
Copy link
Collaborator

@sgreenbury sgreenbury Oct 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps create a single lock to use throughout function?

return Ok(root_candidates.lock().unwrap().get(&date).cloned().unwrap());
}

let result = RootCandidatesResult::new(date, root_did_candidates(date).await?);
debug!("Got root candidates: {:?}", &result);

// Add the result to the cache.
root_candidates.lock().unwrap().insert(date, result.clone());
Ok(result)
}

async fn block_timestamp(height: u64) -> Result<TimestampResult, TrustchainHTTPError> {
debug!("Getting unix timestamp for block height: {0}", height);

let timestamp = time_at_block_height(height, None)
.map_err(|err| TrustchainRootError::FailedToParseBlockHeight(err.to_string()))?;
debug!("Got block timestamp: {:?}", &timestamp);
Ok(TimestampResult { timestamp })
}
}

#[derive(Deserialize, Debug)]
/// Struct for deserializing root event `year` from handler's query param.
pub struct RootEventYear {
year: i32,
}

#[derive(Deserialize, Debug)]
/// Struct for deserializing root event `month` from handler's query param.
pub struct RootEventMonth {
month: u32,
}

#[derive(Deserialize, Debug)]
/// Struct for deserializing root event `day` from handler's query param.
pub struct RootEventDay {
day: u32,
}

impl TrustchainRootHTTPHandler {
/// Handles a GET request for root DID candidates.
pub async fn get_root_candidates(
Query(year): Query<RootEventYear>,
Query(month): Query<RootEventMonth>,
Query(day): Query<RootEventDay>,
State(app_state): State<Arc<AppState>>,
) -> impl IntoResponse {
debug!(
"Received date for root DID candidates: {:?}-{:?}-{:?}",
year, month, day
);

let date = chrono::NaiveDate::from_ymd_opt(year.year, month.month, day.day);
if date.is_none() {
return Err(TrustchainHTTPError::RootError(
TrustchainRootError::InvalidDate(year.year, month.month, day.day),
));
}
TrustchainRootHTTPHandler::root_candidates(date.unwrap(), &app_state.root_candidates)
.await
.map(|vec| (StatusCode::OK, Json(vec)))
}

/// Handles a GET request for a transaction timestamp.
pub async fn get_block_timestamp(Path(height): Path<String>) -> impl IntoResponse {
debug!("Received block height for timestamp: {:?}", height.as_str());
let block_height = height.parse::<u64>();

if block_height.is_err() {
return Err(TrustchainHTTPError::RootError(
TrustchainRootError::FailedToParseBlockHeight(height),
));
}

TrustchainRootHTTPHandler::block_timestamp(block_height.unwrap())
.await
.map(|result| (StatusCode::OK, Json(result)))
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
/// Serializable type representing the result of a request for root DID candidates on a given date.
pub struct RootCandidatesResult {
date: NaiveDate,
root_candidates: Vec<RootCandidate>,
}

impl RootCandidatesResult {
pub fn new(date: NaiveDate, root_candidates: Vec<RootCandidate>) -> Self {
Self {
date,
root_candidates,
}
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
/// Serializable type representing the result of a request for root DID candidates on a given date.
pub struct TimestampResult {
timestamp: Timestamp,
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{config::HTTPConfig, server::TrustchainRouter};
use axum_test_helper::TestClient;

#[tokio::test]
#[ignore = "requires MongoDB and Bitcoin RPC"]
async fn test_root_candidates() {
let app = TrustchainRouter::from(HTTPConfig::default()).into_router();
let client = TestClient::new(app);

// Invalid date in request:
let uri = "/root?year=2022&month=10&day=40".to_string();
let response = client.get(&uri).send().await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
assert_eq!(
response.text().await,
r#"{"error":"Trustchain root error: Invalid date: 2022-10-40"}"#.to_string()
);

// Valid request:
let uri = "/root?year=2022&month=10&day=20".to_string();
let response = client.get(&uri).send().await;
assert_eq!(response.status(), StatusCode::OK);

let result: RootCandidatesResult = serde_json::from_str(&response.text().await).unwrap();

assert_eq!(result.date, NaiveDate::from_ymd_opt(2022, 10, 20).unwrap());

assert_eq!(
result.root_candidates[16].did,
"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"
);
assert_eq!(
result.root_candidates[16].txid,
"9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c"
);
}

#[tokio::test]
#[ignore = "requires MongoDB and Bitcoin RPC"]
async fn test_block_timestamp() {
let app = TrustchainRouter::from(HTTPConfig::default()).into_router();
let client = TestClient::new(app);

// Invalid block height in request:
let uri = "/root/timestamp/2377xyz".to_string();
let response = client.get(&uri).send().await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
assert_eq!(
response.text().await,
r#"{"error":"Trustchain root error: Failed to parse block height: 2377xyz"}"#
.to_string()
);

// Invalid block height in request:
let uri = "/root/timestamp/237744522222".to_string();
let response = client.get(&uri).send().await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
assert!(response.text().await.contains("integer out of range"));

// Valid request:
let uri = "/root/timestamp/2377445".to_string();
let response = client.get(&uri).send().await;
assert_eq!(response.status(), StatusCode::OK);

let result: TimestampResult = serde_json::from_str(&response.text().await).unwrap();

assert_eq!(result.timestamp, 1666265405);
}
}
12 changes: 11 additions & 1 deletion trustchain-http/src/server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::config::http_config;
use crate::middleware::validate_did;
use crate::{config::HTTPConfig, issuer, resolver, state::AppState, static_handlers, verifier};
use crate::{
config::HTTPConfig, issuer, resolver, root, state::AppState, static_handlers, verifier,
};
use axum::routing::IntoMakeService;
use axum::{middleware, routing::get, Router};
use axum_server::tls_rustls::RustlsConfig;
Expand Down Expand Up @@ -85,6 +87,14 @@ impl TrustchainRouter {
get(resolver::TrustchainHTTPHandler::get_verification_bundle)
.layer(ServiceBuilder::new().layer(middleware::from_fn(validate_did))),
)
.route(
"/root",
get(root::TrustchainRootHTTPHandler::get_root_candidates),
)
.route(
"/root/timestamp/:height",
get(root::TrustchainRootHTTPHandler::get_block_timestamp),
)
.with_state(shared_state),
}
}
Expand Down
Loading
Loading