Skip to content
Merged
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
210 changes: 125 additions & 85 deletions rust/signed_doc/src/validator/rules/chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
use anyhow::ensure;
use catalyst_signed_doc_spec::{
is_required::IsRequired,
metadata::{chain::Chain, collaborators::Collaborators},
metadata::{chain::Chain as ChainSpec, collaborators::Collaborators},
};

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

#[cfg(test)]
mod tests;
Expand All @@ -26,7 +29,7 @@ pub(crate) enum ChainRule {
impl ChainRule {
/// Generating `ChainRule` from specs
pub(crate) fn new(
spec: &Chain,
spec: &ChainSpec,
collaborators_spec: &Collaborators,
) -> anyhow::Result<Self> {
let optional = match spec.required {
Expand Down Expand Up @@ -56,9 +59,6 @@ impl ChainRule {
{
let chain = doc.doc_meta().chain();

// TODO: the current implementation is only for the direct chained doc,
// make it recursively checks the entire chain with the same `id` docs.

if let Self::Specified { optional } = self {
if chain.is_none() && !optional {
doc.report()
Expand All @@ -68,85 +68,7 @@ impl ChainRule {

// perform integrity validation
if let Some(doc_chain) = chain {
if doc_chain.document_ref().is_none() && doc_chain.height() != 0 {
doc.report().functional_validation(
"The chain height must be zero when there is no chained doc",
"Chained Documents validation",
);
return Ok(false);
}
if doc_chain.height() == 0 && doc_chain.document_ref().is_some() {
doc.report().functional_validation(
"The next Chained Document must not exist while the height is zero",
"Chained Documents validation",
);
return Ok(false);
}

if let Some(chained_ref) = doc_chain.document_ref() {
let Some(chained_doc) = provider.try_get_doc(chained_ref).await? else {
doc.report().other(
&format!(
"Cannot find the Chained Document ({chained_ref}) from the provider"
),
"Chained Documents validation",
);
return Ok(false);
};

// have the same id as the document being chained to.
if chained_doc.doc_id()? != doc.doc_id()? {
doc.report().functional_validation(
"Must have the same id as the document being chained to",
"Chained Documents validation",
);
return Ok(false);
}

// have a ver that is greater than the ver being chained to.
if chained_doc.doc_ver()? > doc.doc_ver()? {
doc.report().functional_validation(
"Must have a ver that is greater than the ver being chained to",
"Chained Documents validation",
);
return Ok(false);
}

// have the same type as the chained document.
if chained_doc.doc_type()? != doc.doc_type()? {
doc.report().functional_validation(
"Must have the same type as the chained document",
"Chained Documents validation",
);
return Ok(false);
}

if let Some(chained_height) =
chained_doc.doc_meta().chain().map(crate::Chain::height)
{
// chain doc must not be negative
if chained_height < 0 {
doc.report().functional_validation(
"The height of the document being chained to must be positive number",
"Chained Documents validation",
);
return Ok(false);
}

// have its absolute height exactly one more than the height of the
// document being chained to.
if !matches!(
i32::abs(doc_chain.height()).checked_sub(i32::abs(chained_height)),
Some(1)
) {
doc.report().functional_validation(
"Must have its absolute height exactly one more than the height of the document being chained to",
"Chained Documents validation",
);
return Ok(false);
}
}
}
return Self::chain_check(doc_chain, doc, provider).await;
}
}
if let Self::NotSpecified = self
Expand All @@ -167,4 +89,122 @@ impl ChainRule {

Ok(true)
}

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

if doc_chain.document_ref().is_none() && doc_chain.height() != 0 {
doc.report().functional_validation(
"The chain height must be zero when there is no chained doc",
CONTEXT,
);
return Ok(false);
}
if doc_chain.height() == 0 && doc_chain.document_ref().is_some() {
doc.report().functional_validation(
"The next Chained Document must not exist while the height is zero",
CONTEXT,
);
return Ok(false);
}

if let Some(chained_ref) = doc_chain.document_ref() {
let Ok(expected_doc_type) = doc.doc_type() else {
doc.report().missing_field("type", CONTEXT);
return Ok(false);
};

let chain_validator = |chained_doc: &CatalystSignedDocument| {
let Ok(doc_id) = doc.doc_id() else {
doc.report()
.missing_field("id", "Missing id field in the document");
return false;
};
let Ok(chained_id) = chained_doc.doc_id() else {
doc.report()
.missing_field("id", "Missing id field in the chained document");
return false;
};
// have the same id as the document being chained to.
if chained_id != doc_id {
doc.report().functional_validation(
"Must have the same id as the document being chained to",
CONTEXT,
);
return false;
}

let Ok(doc_ver) = doc.doc_ver() else {
doc.report()
.missing_field("ver", "Missing ver field in the document");
return false;
};
let Ok(chained_ver) = chained_doc.doc_ver() else {
doc.report()
.missing_field("ver", "Missing ver field in the chained document");
return false;
};
// have a ver that is greater than the ver being chained to.
if chained_ver > doc_ver {
doc.report().functional_validation(
"Must have a ver that is greater than the ver being chained to",
CONTEXT,
);
return false;
}

let Some(chained_height) = chained_doc.doc_meta().chain().map(crate::Chain::height)
else {
doc.report()
.missing_field("chain", "Missing chain field in the chained document");
return false;
};

// chain doc must not be negative
if chained_height < 0 {
doc.report().functional_validation(
"The height of the document being chained to must be positive number",
CONTEXT,
);
return false;
}

// have its absolute height exactly one more than the height of the
// document being chained to.
if !matches!(
i32::abs(doc_chain.height()).checked_sub(i32::abs(chained_height)),
Some(1)
) {
doc.report().functional_validation(
"Must have its absolute height exactly one more than the height of the document being chained to",
CONTEXT,
);
return false;
}

true
};

return doc_refs_check(
&vec![chained_ref.clone()].into(),
std::slice::from_ref(expected_doc_type),
false,
"chain",
provider,
doc.report(),
chain_validator,
)
.await;
}

Ok(true)
}
}
36 changes: 12 additions & 24 deletions rust/signed_doc/src/validator/rules/doc_ref/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ where
for dr in doc_refs.iter() {
if let Some(ref ref_doc) = provider.try_get_doc(dr).await? {
let is_valid = referenced_doc_type_check(ref_doc, exp_ref_types, field_name, report)
&& referenced_doc_id_and_ver_check(ref_doc, dr, field_name, report)
&& referenced_doc_ref_check(ref_doc, dr, field_name, report)
&& validator(ref_doc);

if !is_valid {
Expand All @@ -167,39 +167,27 @@ where

/// Validation check that the provided `ref_doc` is a correct referenced document found by
/// `original_doc_ref`
fn referenced_doc_id_and_ver_check(
fn referenced_doc_ref_check(
ref_doc: &CatalystSignedDocument,
original_doc_ref: &DocumentRef,
field_name: &str,
report: &ProblemReport,
) -> bool {
let Ok(id) = ref_doc.doc_id() else {
report.missing_field(
"id",
&format!("Referenced document validation for the `{field_name}` field"),
let context = &format!("Referenced document validation for the `{field_name}` field");
let Ok(doc_ref) = ref_doc.doc_ref() else {
report.functional_validation(
"Cannot calculate a document reference of the fetched document",
context,
);
return false;
};

let Ok(ver) = ref_doc.doc_ver() else {
report.missing_field(
"ver",
&format!("Referenced document validation for the `{field_name}` field"),
);
return false;
};

// id and version must match the values in ref doc
if &id != original_doc_ref.id() && &ver != original_doc_ref.ver() {
if &doc_ref != original_doc_ref {
report.invalid_value(
"id and version",
&format!("id: {id}, ver: {ver}"),
&format!(
"id: {}, ver: {}",
original_doc_ref.id(),
original_doc_ref.ver()
),
&format!("Referenced document validation for the `{field_name}` field"),
"document reference",
&format!("{doc_ref}",),
&format!("{original_doc_ref}",),
context,
);
return false;
}
Expand Down
12 changes: 4 additions & 8 deletions rust/signed_doc/src/validator/rules/doc_ref/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,23 +141,19 @@ use crate::{
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
.with_metadata_field(SupportedField::Type(exp_types[0].clone()))
.build();

let new_ref = create_dummy_doc_ref();
let new_ref = DocumentRef::new(
ref_doc.doc_id().unwrap(),
ref_doc.doc_ver().unwrap(),
new_ref.doc_locator().clone()
);
provider.add_document_with_ref(new_ref, &ref_doc);
provider.add_document_with_ref(new_ref.clone(), &ref_doc);

Builder::new()
.with_metadata_field(SupportedField::Ref(
vec![ref_doc.doc_ref().unwrap()].into(),
vec![new_ref].into(),
))
.build()
}
=> false
;
"invalid reference to the document, which has different id and ver fields as stated in the `ref` field"
"invalid reference in the `ref` field to the document, which is different with the fetched document"
)]
#[test_case(
|_, _| {
Expand Down