Skip to content

Commit 9f3f3a3

Browse files
authored
feat(rust/signed-doc): Catalyst Signed Documents extended validation capabilities (#680)
* try * make CatalystSignedDocumentProvider and CatalystIdProvider dyn compatible * wip * wip * wip * use async_trait * fix * fix * fix * fix * fix * added `extend_rules_per_document` fn
1 parent f3dea0f commit 9f3f3a3

File tree

32 files changed

+549
-372
lines changed

32 files changed

+549
-372
lines changed

rust/signed_doc/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ cid = "0.11.1"
4242
multihash = { version = "0.19.3", features = ["serde-codec"] }
4343
sha2 = "0.10"
4444
multibase = "0.9.2"
45+
async-trait = "0.1.89"
46+
dashmap = "6.1.0"
4547

4648
[dev-dependencies]
4749
base64-url = "3.0.0"

rust/signed_doc/src/providers.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,46 @@
11
//! Providers traits, which are used during different validation procedures.
22
3-
use std::{future::Future, time::Duration};
3+
use std::time::Duration;
44

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

88
use crate::{CatalystSignedDocument, DocumentRef};
99

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

2122
/// `CatalystSignedDocument` Provider trait
23+
#[async_trait::async_trait]
2224
pub trait CatalystSignedDocumentProvider: Send + Sync {
2325
/// Try to get `CatalystSignedDocument` from document reference
24-
fn try_get_doc(
26+
async fn try_get_doc(
2527
&self,
2628
doc_ref: &DocumentRef,
27-
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;
29+
) -> anyhow::Result<Option<CatalystSignedDocument>>;
2830

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

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

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

56+
/// Super trait of `CatalystSignedDocumentProvider` and `CatalystIdProvider`
57+
pub trait CatalystSignedDocumentAndCatalystIdProvider:
58+
CatalystSignedDocumentProvider + CatalystIdProvider
59+
{
60+
}
61+
62+
impl<T: CatalystSignedDocumentProvider + CatalystIdProvider>
63+
CatalystSignedDocumentAndCatalystIdProvider for T
64+
{
65+
}
66+
5467
pub mod tests {
5568
//! Simple providers implementation just for the testing purposes
5669
@@ -117,6 +130,7 @@ pub mod tests {
117130
}
118131
}
119132

133+
#[async_trait::async_trait]
120134
impl CatalystSignedDocumentProvider for TestCatalystProvider {
121135
async fn try_get_doc(
122136
&self,
@@ -158,6 +172,7 @@ pub mod tests {
158172
}
159173
}
160174

175+
#[async_trait::async_trait]
161176
impl CatalystIdProvider for TestCatalystProvider {
162177
async fn try_get_registered_key(
163178
&self,

rust/signed_doc/src/validator/mod.rs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,44 @@
22
33
pub(crate) mod rules;
44

5-
use std::{collections::HashMap, sync::LazyLock};
5+
use std::{fmt::Debug, sync::LazyLock};
66

7-
use rules::Rules;
7+
use dashmap::DashMap;
8+
use futures::{StreamExt, TryStreamExt};
89

910
use crate::{
1011
CatalystSignedDocument,
1112
metadata::DocType,
12-
providers::{CatalystIdProvider, CatalystSignedDocumentProvider},
13+
providers::{
14+
CatalystIdProvider, CatalystSignedDocumentAndCatalystIdProvider,
15+
CatalystSignedDocumentProvider,
16+
},
17+
validator::rules::documents_rules_from_spec,
1318
};
1419

20+
/// `CatalystSignedDocument` validation rule trait
21+
#[async_trait::async_trait]
22+
pub trait CatalystSignedDocumentValidationRule: 'static + Send + Sync + Debug {
23+
/// Validates `CatalystSignedDocument`, return `false` if the provided
24+
/// `CatalystSignedDocument` violates some validation rules with properly filling the
25+
/// problem report.
26+
async fn check(
27+
&self,
28+
doc: &CatalystSignedDocument,
29+
provider: &dyn CatalystSignedDocumentAndCatalystIdProvider,
30+
) -> anyhow::Result<bool>;
31+
}
32+
33+
/// Struct represented a collection of rules
34+
pub(crate) type Rules = Vec<Box<dyn CatalystSignedDocumentValidationRule>>;
35+
1536
/// A table representing a full set or validation rules per document id.
16-
static DOCUMENT_RULES: LazyLock<HashMap<DocType, Rules>> = LazyLock::new(document_rules_init);
37+
static DOCUMENT_RULES: LazyLock<DashMap<DocType, Rules>> = LazyLock::new(document_rules_init);
1738

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

@@ -56,7 +77,26 @@ where
5677
);
5778
return Ok(false);
5879
};
59-
rules.check(doc, provider).await
80+
81+
let iter = rules.iter().map(|v| v.check(doc, provider));
82+
let res = futures::stream::iter(iter)
83+
.buffer_unordered(rules.len())
84+
.try_collect::<Vec<_>>()
85+
.await?
86+
.iter()
87+
.all(|res| *res);
88+
Ok(res)
89+
}
90+
91+
/// Extend the current defined validation rules set for the provided document type.
92+
pub fn extend_rules_per_document(
93+
doc_type: DocType,
94+
rule: impl CatalystSignedDocumentValidationRule,
95+
) {
96+
DOCUMENT_RULES
97+
.entry(doc_type)
98+
.or_default()
99+
.push(Box::new(rule));
60100
}
61101

62102
#[cfg(test)]

rust/signed_doc/src/validator/rules/chain/mod.rs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use catalyst_signed_doc_spec::{
77
};
88

99
use crate::{
10-
CatalystSignedDocument, Chain, providers::CatalystSignedDocumentProvider,
11-
validator::rules::doc_ref::doc_refs_check,
10+
CatalystSignedDocument, Chain,
11+
providers::{CatalystSignedDocumentAndCatalystIdProvider, CatalystSignedDocumentProvider},
12+
validator::{CatalystSignedDocumentValidationRule, rules::doc_ref::doc_refs_check},
1213
};
1314

1415
#[cfg(test)]
@@ -26,6 +27,17 @@ pub(crate) enum ChainRule {
2627
NotSpecified,
2728
}
2829

30+
#[async_trait::async_trait]
31+
impl CatalystSignedDocumentValidationRule for ChainRule {
32+
async fn check(
33+
&self,
34+
doc: &CatalystSignedDocument,
35+
provider: &dyn CatalystSignedDocumentAndCatalystIdProvider,
36+
) -> anyhow::Result<bool> {
37+
self.check_inner(doc, provider).await
38+
}
39+
}
40+
2941
impl ChainRule {
3042
/// Generating `ChainRule` from specs
3143
pub(crate) fn new(
@@ -49,14 +61,11 @@ impl ChainRule {
4961
}
5062

5163
/// Field validation rule
52-
pub(crate) async fn check<Provider>(
64+
async fn check_inner(
5365
&self,
5466
doc: &CatalystSignedDocument,
55-
provider: &Provider,
56-
) -> anyhow::Result<bool>
57-
where
58-
Provider: CatalystSignedDocumentProvider,
59-
{
67+
provider: &dyn CatalystSignedDocumentAndCatalystIdProvider,
68+
) -> anyhow::Result<bool> {
6069
let chain = doc.doc_meta().chain();
6170

6271
if let Self::Specified { optional } = self {
@@ -91,14 +100,11 @@ impl ChainRule {
91100
}
92101

93102
/// `chain` metadata field checks
94-
async fn chain_check<Provider>(
103+
async fn chain_check(
95104
doc_chain: &Chain,
96105
doc: &CatalystSignedDocument,
97-
provider: &Provider,
98-
) -> anyhow::Result<bool>
99-
where
100-
Provider: CatalystSignedDocumentProvider,
101-
{
106+
provider: &dyn CatalystSignedDocumentProvider,
107+
) -> anyhow::Result<bool> {
102108
const CONTEXT: &str = "Chained Documents validation";
103109

104110
if doc_chain.document_ref().is_none() && doc_chain.height() != 0 {

rust/signed_doc/src/validator/rules/chain/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,5 +291,5 @@ async fn test_invalid_chained_documents(
291291
) -> bool {
292292
let rule = ChainRule::Specified { optional: false };
293293

294-
rule.check(&doc, &provider).await.unwrap()
294+
rule.check_inner(&doc, &provider).await.unwrap()
295295
}

rust/signed_doc/src/validator/rules/collaborators/mod.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ mod tests;
55

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

8-
use crate::CatalystSignedDocument;
8+
use crate::{
9+
CatalystSignedDocument, providers::CatalystSignedDocumentAndCatalystIdProvider,
10+
validator::CatalystSignedDocumentValidationRule,
11+
};
912

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

25+
#[async_trait::async_trait]
26+
impl CatalystSignedDocumentValidationRule for CollaboratorsRule {
27+
async fn check(
28+
&self,
29+
doc: &CatalystSignedDocument,
30+
_provider: &dyn CatalystSignedDocumentAndCatalystIdProvider,
31+
) -> anyhow::Result<bool> {
32+
Ok(self.check_inner(doc))
33+
}
34+
}
35+
2236
impl CollaboratorsRule {
2337
/// Generating `CollaboratorsRule` from specs
2438
pub(crate) fn new(spec: &Collaborators) -> Self {
@@ -34,11 +48,10 @@ impl CollaboratorsRule {
3448
}
3549

3650
/// Field validation rule
37-
#[allow(clippy::unused_async)]
38-
pub(crate) async fn check(
51+
fn check_inner(
3952
&self,
4053
doc: &CatalystSignedDocument,
41-
) -> anyhow::Result<bool> {
54+
) -> bool {
4255
if let Self::Specified { optional } = self
4356
&& doc.doc_meta().collaborators().is_empty()
4457
&& !optional
@@ -47,7 +60,7 @@ impl CollaboratorsRule {
4760
"collaborators",
4861
"Document must have at least one entry in 'collaborators' field",
4962
);
50-
return Ok(false);
63+
return false;
5164
}
5265
if let Self::NotSpecified = self
5366
&& !doc.doc_meta().collaborators().is_empty()
@@ -64,9 +77,9 @@ impl CollaboratorsRule {
6477
),
6578
"Document does not expect to have a 'collaborators' field",
6679
);
67-
return Ok(false);
80+
return false;
6881
}
6982

70-
Ok(true)
83+
true
7184
}
7285
}

rust/signed_doc/src/validator/rules/collaborators/tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async fn section_rule_specified_optional_test(
3434
let rule = CollaboratorsRule::Specified { optional: true };
3535

3636
let doc = doc_gen();
37-
rule.check(&doc).await.unwrap()
37+
rule.check_inner(&doc)
3838
}
3939

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

6666
let doc = doc_gen();
67-
rule.check(&doc).await.unwrap()
67+
rule.check_inner(&doc)
6868
}
6969

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

9494
let doc = doc_gen();
95-
rule.check(&doc).await.unwrap()
95+
rule.check_inner(&doc)
9696
}

0 commit comments

Comments
 (0)