Skip to content
Merged
2 changes: 2 additions & 0 deletions rust/signed_doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ cid = "0.11.1"
multihash = { version = "0.19.3", features = ["serde-codec"] }
sha2 = "0.10"
multibase = "0.9.2"
async-trait = "0.1.89"
dashmap = "6.1.0"

[dev-dependencies]
base64-url = "3.0.0"
Expand Down
33 changes: 24 additions & 9 deletions rust/signed_doc/src/providers.rs
Original file line number Diff line number Diff line change
@@ -1,44 +1,46 @@
//! Providers traits, which are used during different validation procedures.

use std::{future::Future, time::Duration};
use std::time::Duration;

use catalyst_types::{catalyst_id::CatalystId, uuid::UuidV7};
use ed25519_dalek::VerifyingKey;

use crate::{CatalystSignedDocument, DocumentRef};

/// `CatalystId` Provider trait
#[async_trait::async_trait]
pub trait CatalystIdProvider: Send + Sync {
/// Try to get `VerifyingKey` by the provided `CatalystId` and corresponding `RoleId`
/// and `KeyRotation` Return `None` if the provided `CatalystId` with the
/// corresponding `RoleId` and `KeyRotation` has not been registered.
fn try_get_registered_key(
async fn try_get_registered_key(
&self,
kid: &CatalystId,
) -> impl Future<Output = anyhow::Result<Option<VerifyingKey>>> + Send;
) -> anyhow::Result<Option<VerifyingKey>>;
}

/// `CatalystSignedDocument` Provider trait
#[async_trait::async_trait]
pub trait CatalystSignedDocumentProvider: Send + Sync {
/// Try to get `CatalystSignedDocument` from document reference
fn try_get_doc(
async fn try_get_doc(
&self,
doc_ref: &DocumentRef,
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;
) -> anyhow::Result<Option<CatalystSignedDocument>>;

/// Try to get the last known version of the `CatalystSignedDocument`, same
/// `id` and the highest known `ver`.
fn try_get_last_doc(
async fn try_get_last_doc(
&self,
id: UuidV7,
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;
) -> anyhow::Result<Option<CatalystSignedDocument>>;

/// Try to get the first known version of the `CatalystSignedDocument`, `id` and `ver`
/// are equal.
fn try_get_first_doc(
async fn try_get_first_doc(
&self,
id: UuidV7,
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;
) -> anyhow::Result<Option<CatalystSignedDocument>>;

/// Returns a future threshold value, which is used in the validation of the `ver`
/// field that it is not too far in the future.
Expand All @@ -51,6 +53,17 @@ pub trait CatalystSignedDocumentProvider: Send + Sync {
fn past_threshold(&self) -> Option<Duration>;
}

/// Super trait of `CatalystSignedDocumentProvider` and `CatalystIdProvider`
pub trait CatalystSignedDocumentAndCatalystIdProvider:
CatalystSignedDocumentProvider + CatalystIdProvider
{
}

impl<T: CatalystSignedDocumentProvider + CatalystIdProvider>
CatalystSignedDocumentAndCatalystIdProvider for T
{
}

pub mod tests {
//! Simple providers implementation just for the testing purposes

Expand Down Expand Up @@ -117,6 +130,7 @@ pub mod tests {
}
}

#[async_trait::async_trait]
impl CatalystSignedDocumentProvider for TestCatalystProvider {
async fn try_get_doc(
&self,
Expand Down Expand Up @@ -158,6 +172,7 @@ pub mod tests {
}
}

#[async_trait::async_trait]
impl CatalystIdProvider for TestCatalystProvider {
async fn try_get_registered_key(
&self,
Expand Down
54 changes: 47 additions & 7 deletions rust/signed_doc/src/validator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,44 @@

pub(crate) mod rules;

use std::{collections::HashMap, sync::LazyLock};
use std::{fmt::Debug, sync::LazyLock};

use rules::Rules;
use dashmap::DashMap;
use futures::{StreamExt, TryStreamExt};

use crate::{
CatalystSignedDocument,
metadata::DocType,
providers::{CatalystIdProvider, CatalystSignedDocumentProvider},
providers::{
CatalystIdProvider, CatalystSignedDocumentAndCatalystIdProvider,
CatalystSignedDocumentProvider,
},
validator::rules::documents_rules_from_spec,
};

/// `CatalystSignedDocument` validation rule trait
#[async_trait::async_trait]
pub trait CatalystSignedDocumentValidationRule: 'static + Send + Sync + Debug {
/// Validates `CatalystSignedDocument`, return `false` if the provided
/// `CatalystSignedDocument` violates some validation rules with properly filling the
/// problem report.
async fn check(
&self,
doc: &CatalystSignedDocument,
provider: &dyn CatalystSignedDocumentAndCatalystIdProvider,
) -> anyhow::Result<bool>;
}

/// Struct represented a collection of rules
pub(crate) type Rules = Vec<Box<dyn CatalystSignedDocumentValidationRule>>;

/// A table representing a full set or validation rules per document id.
static DOCUMENT_RULES: LazyLock<HashMap<DocType, Rules>> = LazyLock::new(document_rules_init);
static DOCUMENT_RULES: LazyLock<DashMap<DocType, Rules>> = LazyLock::new(document_rules_init);

/// `DOCUMENT_RULES` initialization function
#[allow(clippy::expect_used)]
fn document_rules_init() -> HashMap<DocType, Rules> {
let document_rules_map: HashMap<DocType, Rules> = Rules::documents_rules()
fn document_rules_init() -> DashMap<DocType, Rules> {
let document_rules_map: DashMap<DocType, Rules> = documents_rules_from_spec()
.expect("cannot fail to initialize validation rules")
.collect();

Expand Down Expand Up @@ -56,7 +77,26 @@ where
);
return Ok(false);
};
rules.check(doc, provider).await

let iter = rules.iter().map(|v| v.check(doc, provider));
let res = futures::stream::iter(iter)
.buffer_unordered(rules.len())
.try_collect::<Vec<_>>()
.await?
.iter()
.all(|res| *res);
Ok(res)
}

/// Extend the current defined validation rules set for the provided document type.
pub fn extend_rules_per_document(
doc_type: DocType,
rule: impl CatalystSignedDocumentValidationRule,
) {
DOCUMENT_RULES
.entry(doc_type)
.or_default()
.push(Box::new(rule));
}

#[cfg(test)]
Expand Down
34 changes: 20 additions & 14 deletions rust/signed_doc/src/validator/rules/chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use catalyst_signed_doc_spec::{
};

use crate::{
CatalystSignedDocument, Chain, providers::CatalystSignedDocumentProvider,
validator::rules::doc_ref::doc_refs_check,
CatalystSignedDocument, Chain,
providers::{CatalystSignedDocumentAndCatalystIdProvider, CatalystSignedDocumentProvider},
validator::{CatalystSignedDocumentValidationRule, rules::doc_ref::doc_refs_check},
};

#[cfg(test)]
Expand All @@ -26,6 +27,17 @@ pub(crate) enum ChainRule {
NotSpecified,
}

#[async_trait::async_trait]
impl CatalystSignedDocumentValidationRule for ChainRule {
async fn check(
&self,
doc: &CatalystSignedDocument,
provider: &dyn CatalystSignedDocumentAndCatalystIdProvider,
) -> anyhow::Result<bool> {
self.check_inner(doc, provider).await
}
}

impl ChainRule {
/// Generating `ChainRule` from specs
pub(crate) fn new(
Expand All @@ -49,14 +61,11 @@ impl ChainRule {
}

/// Field validation rule
pub(crate) async fn check<Provider>(
async fn check_inner(
&self,
doc: &CatalystSignedDocument,
provider: &Provider,
) -> anyhow::Result<bool>
where
Provider: CatalystSignedDocumentProvider,
{
provider: &dyn CatalystSignedDocumentAndCatalystIdProvider,
) -> anyhow::Result<bool> {
let chain = doc.doc_meta().chain();

if let Self::Specified { optional } = self {
Expand Down Expand Up @@ -91,14 +100,11 @@ impl ChainRule {
}

/// `chain` metadata field checks
async fn chain_check<Provider>(
async fn chain_check(
doc_chain: &Chain,
doc: &CatalystSignedDocument,
provider: &Provider,
) -> anyhow::Result<bool>
where
Provider: CatalystSignedDocumentProvider,
{
provider: &dyn CatalystSignedDocumentProvider,
) -> anyhow::Result<bool> {
const CONTEXT: &str = "Chained Documents validation";

if doc_chain.document_ref().is_none() && doc_chain.height() != 0 {
Expand Down
2 changes: 1 addition & 1 deletion rust/signed_doc/src/validator/rules/chain/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,5 +291,5 @@ async fn test_invalid_chained_documents(
) -> bool {
let rule = ChainRule::Specified { optional: false };

rule.check(&doc, &provider).await.unwrap()
rule.check_inner(&doc, &provider).await.unwrap()
}
27 changes: 20 additions & 7 deletions rust/signed_doc/src/validator/rules/collaborators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ mod tests;

use catalyst_signed_doc_spec::{is_required::IsRequired, metadata::collaborators::Collaborators};

use crate::CatalystSignedDocument;
use crate::{
CatalystSignedDocument, providers::CatalystSignedDocumentAndCatalystIdProvider,
validator::CatalystSignedDocumentValidationRule,
};

/// `collaborators` field validation rule
#[derive(Debug)]
Expand All @@ -19,6 +22,17 @@ pub(crate) enum CollaboratorsRule {
NotSpecified,
}

#[async_trait::async_trait]
impl CatalystSignedDocumentValidationRule for CollaboratorsRule {
async fn check(
&self,
doc: &CatalystSignedDocument,
_provider: &dyn CatalystSignedDocumentAndCatalystIdProvider,
) -> anyhow::Result<bool> {
Ok(self.check_inner(doc))
}
}

impl CollaboratorsRule {
/// Generating `CollaboratorsRule` from specs
pub(crate) fn new(spec: &Collaborators) -> Self {
Expand All @@ -34,11 +48,10 @@ impl CollaboratorsRule {
}

/// Field validation rule
#[allow(clippy::unused_async)]
pub(crate) async fn check(
fn check_inner(
&self,
doc: &CatalystSignedDocument,
) -> anyhow::Result<bool> {
) -> bool {
if let Self::Specified { optional } = self
&& doc.doc_meta().collaborators().is_empty()
&& !optional
Expand All @@ -47,7 +60,7 @@ impl CollaboratorsRule {
"collaborators",
"Document must have at least one entry in 'collaborators' field",
);
return Ok(false);
return false;
}
if let Self::NotSpecified = self
&& !doc.doc_meta().collaborators().is_empty()
Expand All @@ -64,9 +77,9 @@ impl CollaboratorsRule {
),
"Document does not expect to have a 'collaborators' field",
);
return Ok(false);
return false;
}

Ok(true)
true
}
}
6 changes: 3 additions & 3 deletions rust/signed_doc/src/validator/rules/collaborators/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async fn section_rule_specified_optional_test(
let rule = CollaboratorsRule::Specified { optional: true };

let doc = doc_gen();
rule.check(&doc).await.unwrap()
rule.check_inner(&doc)
}

#[test_case(
Expand Down Expand Up @@ -64,7 +64,7 @@ async fn section_rule_specified_not_optional_test(
let rule = CollaboratorsRule::Specified { optional: false };

let doc = doc_gen();
rule.check(&doc).await.unwrap()
rule.check_inner(&doc)
}

#[test_case(
Expand Down Expand Up @@ -92,5 +92,5 @@ async fn section_rule_not_specified_test(doc_gen: impl FnOnce() -> CatalystSigne
let rule = CollaboratorsRule::NotSpecified;

let doc = doc_gen();
rule.check(&doc).await.unwrap()
rule.check_inner(&doc)
}
Loading