diff --git a/rust/signed_doc/src/validator/rules/chain/mod.rs b/rust/signed_doc/src/validator/rules/chain/mod.rs index dfec2c2ac6..198c24e98b 100644 --- a/rust/signed_doc/src/validator/rules/chain/mod.rs +++ b/rust/signed_doc/src/validator/rules/chain/mod.rs @@ -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; @@ -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 { let optional = match spec.required { @@ -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() @@ -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 @@ -167,4 +89,122 @@ impl ChainRule { Ok(true) } + + /// `chain` metadata field checks + async fn chain_check( + doc_chain: &Chain, + doc: &CatalystSignedDocument, + provider: &Provider, + ) -> anyhow::Result + 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) + } } diff --git a/rust/signed_doc/src/validator/rules/doc_ref/mod.rs b/rust/signed_doc/src/validator/rules/doc_ref/mod.rs index 22b6b0a304..414eb876f1 100644 --- a/rust/signed_doc/src/validator/rules/doc_ref/mod.rs +++ b/rust/signed_doc/src/validator/rules/doc_ref/mod.rs @@ -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 { @@ -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; } diff --git a/rust/signed_doc/src/validator/rules/doc_ref/tests.rs b/rust/signed_doc/src/validator/rules/doc_ref/tests.rs index 0b2970295d..aef47609d8 100644 --- a/rust/signed_doc/src/validator/rules/doc_ref/tests.rs +++ b/rust/signed_doc/src/validator/rules/doc_ref/tests.rs @@ -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( |_, _| {