Skip to content

Commit 7aa7eaf

Browse files
apskhemMr-Leshiy
andauthored
feat(rust/signed-doc): Add chain field metadata validation rule (#573)
* feat: initial * feat: initial integrity validation * feat: document validation * feat: updated provider * feat: validation message * feat: complete chain validation * feat: complete chain validation * feat: remaining case * feat: initial test * feat: listing tests * feat: basic rules * feat: tmp valid test * feat: valid test pass * feat: invalid cases finalized * feat: chain rule * chore: fmtfix * fix: spellcheck * fix: comments * chore: lintfix * chore: lintfix * feat: height validation * fix: deferred report * fix: cspell * chore: minor comment * feat: minor validation * chore: minor * Update rust/signed_doc/src/validator/rules/chain/mod.rs Co-authored-by: Alex Pozhylenkov <leshiy12345678@gmail.com> * fix: comments * fix: chain link validation --------- Co-authored-by: Alex Pozhylenkov <leshiy12345678@gmail.com>
1 parent 25c3728 commit 7aa7eaf

File tree

8 files changed

+554
-1
lines changed

8 files changed

+554
-1
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//! `signed_doc.json` "chain" field JSON definition
2+
3+
use crate::is_required::IsRequired;
4+
5+
/// `signed_doc.json` "chain" field JSON object
6+
#[derive(serde::Deserialize)]
7+
pub struct Chain {
8+
pub required: IsRequired,
9+
}

rust/catalyst-signed-doc-spec/src/metadata/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! `metadata` field definition
22
3+
pub mod chain;
34
pub mod doc_ref;
45
pub mod parameters;
56
pub mod reply;
@@ -14,4 +15,5 @@ pub struct Metadata {
1415
pub doc_ref: doc_ref::Ref,
1516
pub reply: reply::Reply,
1617
pub parameters: parameters::Parameters,
18+
pub chain: chain::Chain,
1719
}

rust/signed_doc/src/metadata/chain.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,32 @@ pub struct Chain {
3434
document_ref: Option<DocumentRef>,
3535
}
3636

37+
impl Chain {
38+
/// Creates a new `Chain`.
39+
#[must_use]
40+
pub fn new(
41+
height: i32,
42+
document_ref: Option<DocumentRef>,
43+
) -> Self {
44+
Self {
45+
height,
46+
document_ref,
47+
}
48+
}
49+
50+
/// Gets `height`.
51+
#[must_use]
52+
pub fn height(&self) -> i32 {
53+
self.height
54+
}
55+
56+
/// Gets `document_ref`.
57+
#[must_use]
58+
pub fn document_ref(&self) -> Option<&DocumentRef> {
59+
self.document_ref.as_ref()
60+
}
61+
}
62+
3763
impl Display for Chain {
3864
fn fmt(
3965
&self,

rust/signed_doc/src/metadata/document_refs/doc_ref.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use cbork_utils::{array::Array, decode_context::DecodeCtx};
77
use minicbor::{Decode, Encode};
88

99
use super::doc_locator::DocLocator;
10+
use crate::CatalystSignedDocument;
1011

1112
/// Number of item that should be in each document reference instance.
1213
const DOC_REF_ARR_ITEM: u64 = 3;
@@ -57,6 +58,18 @@ impl DocumentRef {
5758
}
5859
}
5960

61+
impl TryFrom<&CatalystSignedDocument> for DocumentRef {
62+
type Error = anyhow::Error;
63+
64+
fn try_from(value: &CatalystSignedDocument) -> Result<Self, Self::Error> {
65+
Ok(Self::new(
66+
value.doc_id()?,
67+
value.doc_ver()?,
68+
DocLocator::default(),
69+
))
70+
}
71+
}
72+
6073
impl Display for DocumentRef {
6174
fn fmt(
6275
&self,
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//! `chain` rule type impl.
2+
3+
use catalyst_signed_doc_spec::{is_required::IsRequired, metadata::chain::Chain, DocSpecs};
4+
5+
use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument};
6+
7+
#[cfg(test)]
8+
mod tests;
9+
10+
/// `chain` field validation rule
11+
#[derive(Debug)]
12+
pub(crate) enum ChainRule {
13+
/// Is 'chain' specified
14+
#[allow(dead_code)]
15+
Specified {
16+
/// optional flag for the `chain` field
17+
optional: bool,
18+
},
19+
/// 'chain' is not specified
20+
NotSpecified,
21+
}
22+
23+
impl ChainRule {
24+
/// Generating `ChainRule` from specs
25+
pub(crate) fn new(
26+
_docs: &DocSpecs,
27+
spec: &Chain,
28+
) -> Self {
29+
let optional = match spec.required {
30+
IsRequired::Yes => false,
31+
IsRequired::Optional => true,
32+
IsRequired::Excluded => {
33+
return Self::NotSpecified;
34+
},
35+
};
36+
37+
Self::Specified { optional }
38+
}
39+
40+
/// Field validation rule
41+
#[allow(clippy::too_many_lines)]
42+
pub(crate) async fn check<Provider>(
43+
&self,
44+
doc: &CatalystSignedDocument,
45+
provider: &Provider,
46+
) -> anyhow::Result<bool>
47+
where
48+
Provider: CatalystSignedDocumentProvider,
49+
{
50+
let chain = doc.doc_meta().chain();
51+
52+
// TODO: the current implementation is only for the direct chained doc,
53+
// make it recursively checks the entire chain with the same `id` docs.
54+
55+
if let Self::Specified { optional } = self {
56+
if chain.is_none() && !optional {
57+
doc.report()
58+
.missing_field("chain", "Document must have 'chain' field");
59+
return Ok(false);
60+
}
61+
62+
// perform integrity validation
63+
if let Some(doc_chain) = chain {
64+
if doc_chain.document_ref().is_none() && doc_chain.height() != 0 {
65+
doc.report().functional_validation(
66+
"The chain height must be zero when there is no chained doc",
67+
"Chained Documents validation",
68+
);
69+
return Ok(false);
70+
}
71+
if doc_chain.height() == 0 && doc_chain.document_ref().is_some() {
72+
doc.report().functional_validation(
73+
"The next Chained Document must not exist while the height is zero",
74+
"Chained Documents validation",
75+
);
76+
return Ok(false);
77+
}
78+
79+
if let Some(chained_ref) = doc_chain.document_ref() {
80+
let Some(chained_doc) = provider.try_get_doc(chained_ref).await? else {
81+
doc.report().other(
82+
&format!(
83+
"Cannot find the Chained Document ({chained_ref}) from the provider"
84+
),
85+
"Chained Documents validation",
86+
);
87+
return Ok(false);
88+
};
89+
90+
// have the same id as the document being chained to.
91+
if chained_doc.doc_id()? != doc.doc_id()? {
92+
doc.report().functional_validation(
93+
"Must have the same id as the document being chained to",
94+
"Chained Documents validation",
95+
);
96+
return Ok(false);
97+
}
98+
99+
// have a ver that is greater than the ver being chained to.
100+
if chained_doc.doc_ver()? > doc.doc_ver()? {
101+
doc.report().functional_validation(
102+
"Must have a ver that is greater than the ver being chained to",
103+
"Chained Documents validation",
104+
);
105+
return Ok(false);
106+
}
107+
108+
// have the same type as the chained document.
109+
if chained_doc.doc_type()? != doc.doc_type()? {
110+
doc.report().functional_validation(
111+
"Must have the same type as the chained document",
112+
"Chained Documents validation",
113+
);
114+
return Ok(false);
115+
}
116+
117+
if let Some(chained_height) =
118+
chained_doc.doc_meta().chain().map(crate::Chain::height)
119+
{
120+
// chain doc must not be negative
121+
if chained_height < 0 {
122+
doc.report().functional_validation(
123+
"The height of the document being chained to must be positive number",
124+
"Chained Documents validation",
125+
);
126+
return Ok(false);
127+
}
128+
129+
// have its absolute height exactly one more than the height of the
130+
// document being chained to.
131+
if !matches!(
132+
i32::abs(doc_chain.height()).checked_sub(i32::abs(chained_height)),
133+
Some(1)
134+
) {
135+
doc.report().functional_validation(
136+
"Must have its absolute height exactly one more than the height of the document being chained to",
137+
"Chained Documents validation",
138+
);
139+
return Ok(false);
140+
}
141+
}
142+
}
143+
}
144+
}
145+
if let Self::NotSpecified = self {
146+
if chain.is_some() {
147+
doc.report().unknown_field(
148+
"chain",
149+
&doc.doc_meta()
150+
.chain()
151+
.iter()
152+
.map(ToString::to_string)
153+
.reduce(|a, b| format!("{a}, {b}"))
154+
.unwrap_or_default(),
155+
"Document does not expect to have 'chain' field",
156+
);
157+
return Ok(false);
158+
}
159+
}
160+
161+
Ok(true)
162+
}
163+
}

0 commit comments

Comments
 (0)