From 8ba820259b1137973689a3548d2f109d0440afb4 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Mon, 19 May 2025 20:26:59 +0700 Subject: [PATCH 01/14] chore: add new line to open pr Signed-off-by: bkioshn --- rust/signed_doc/src/metadata/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index bbbdb1677d7..1c939eb65be 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -259,3 +259,4 @@ impl TryFrom<&Metadata> for coset::Header { Ok(builder.build()) } } + From 749717e4a3559128a7daade4e61e5d8c58648e27 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Mon, 19 May 2025 20:32:00 +0700 Subject: [PATCH 02/14] chore: revert Signed-off-by: bkioshn --- rust/signed_doc/src/metadata/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 1c939eb65be..bbbdb1677d7 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -259,4 +259,3 @@ impl TryFrom<&Metadata> for coset::Header { Ok(builder.build()) } } - From bcf8583179971efa165e03f8937a6f18ef4eb59f Mon Sep 17 00:00:00 2001 From: bkioshn <35752733+bkioshn@users.noreply.github.com> Date: Thu, 22 May 2025 14:44:32 +0700 Subject: [PATCH 03/14] feat(rust/signed-doc): add new type `DocType` (#339) * feat(signed-doc): add new type DocType Signed-off-by: bkioshn * fix(signed-doc): add conversion policy Signed-off-by: bkioshn * fix(signed-doc): doc type Signed-off-by: bkioshn * fix(signed-doc): doc type error Signed-off-by: bkioshn * fix(signed-doc): seperate test Signed-off-by: bkioshn * fix(signed-doc): format Signed-off-by: bkioshn --------- Signed-off-by: bkioshn --- rust/signed_doc/Cargo.toml | 3 +- rust/signed_doc/src/decode_context.rs | 24 ++ rust/signed_doc/src/doc_types/mod.rs | 2 + rust/signed_doc/src/lib.rs | 5 +- rust/signed_doc/src/metadata/doc_type.rs | 333 +++++++++++++++++++++++ rust/signed_doc/src/metadata/mod.rs | 2 + 6 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 rust/signed_doc/src/decode_context.rs create mode 100644 rust/signed_doc/src/metadata/doc_type.rs diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 62c4c6b8f72..7c2412a40eb 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -27,7 +27,8 @@ jsonschema = "0.28.3" jsonpath-rust = "0.7.5" futures = "0.3.31" ed25519-bip32 = "0.4.1" # used by the `mk_signed_doc` cli tool - +tracing = "0.1.40" +thiserror = "2.0.11" [dev-dependencies] base64-url = "3.0.0" diff --git a/rust/signed_doc/src/decode_context.rs b/rust/signed_doc/src/decode_context.rs new file mode 100644 index 00000000000..9667fefd371 --- /dev/null +++ b/rust/signed_doc/src/decode_context.rs @@ -0,0 +1,24 @@ +//! Context used to pass in decoder for additional information. + +use catalyst_types::problem_report::ProblemReport; + +/// Compatibility policy +#[allow(dead_code)] +pub(crate) enum CompatibilityPolicy { + /// Silently allow obsoleted type conversions or non deterministic encoding. + Accept, + /// Allow but log Warnings for all obsoleted type conversions or non deterministic + /// encoding. + Warn, + /// Fail and update problem report when an obsolete type is encountered or the data is + /// not deterministically encoded. + Fail, +} + +/// A context use to pass to decoder. +pub(crate) struct DecodeContext<'r> { + /// Compatibility policy. + pub compatibility_policy: CompatibilityPolicy, + /// Problem report. + pub report: &'r mut ProblemReport, +} diff --git a/rust/signed_doc/src/doc_types/mod.rs b/rust/signed_doc/src/doc_types/mod.rs index 683e36db2f2..15bcb5948f5 100644 --- a/rust/signed_doc/src/doc_types/mod.rs +++ b/rust/signed_doc/src/doc_types/mod.rs @@ -51,3 +51,5 @@ pub const PRIVATE_VOTE_TX_V2_UUID_TYPE: Uuid = /// Immutable ledger block `UuidV4` type. pub const IMMUTABLE_LEDGER_BLOCK_UUID_TYPE: Uuid = Uuid::from_u128(0xD9E7_E6CE_2401_4D7D_9492_F4F7_C642_41C3); +/// Submission Action `UuidV4` type. +pub const SUBMISSION_ACTION: Uuid = Uuid::from_u128(0x7892_7329_CFD9_4EA1_9C71_0E01_9B12_6A65); diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 39908e7f117..c5f4777e506 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -2,6 +2,7 @@ mod builder; mod content; +mod decode_context; pub mod doc_types; mod metadata; pub mod providers; @@ -22,7 +23,9 @@ pub use catalyst_types::{ }; pub use content::Content; use coset::{CborSerializable, Header, TaggedCborSerializable}; -pub use metadata::{ContentEncoding, ContentType, DocumentRef, ExtraFields, Metadata, Section}; +pub use metadata::{ + ContentEncoding, ContentType, DocType, DocumentRef, ExtraFields, Metadata, Section, +}; use minicbor::{decode, encode, Decode, Decoder, Encode}; pub use signature::{CatalystId, Signatures}; diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs new file mode 100644 index 00000000000..c26240df030 --- /dev/null +++ b/rust/signed_doc/src/metadata/doc_type.rs @@ -0,0 +1,333 @@ +//! Document Type. + +use std::fmt::{Display, Formatter}; + +use catalyst_types::{ + problem_report::ProblemReport, + uuid::{CborContext, Uuid, UuidV4}, +}; +use minicbor::{Decode, Decoder, Encode}; +use tracing::warn; + +use crate::{ + decode_context::{CompatibilityPolicy, DecodeContext}, + doc_types::{ + COMMENT_DOCUMENT_UUID_TYPE, PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + PROPOSAL_DOCUMENT_UUID_TYPE, SUBMISSION_ACTION, + }, +}; + +/// List of `UUIDv4` document type. +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct DocType(Vec); + +/// `DocType` Errors. +#[derive(Debug, Clone, thiserror::Error)] +pub enum DocTypeError { + /// Invalid UUID. + #[error("Invalid UUID: {0}")] + InvalidUuid(Uuid), + /// `DocType` cannot be empty. + #[error("DocType cannot be empty")] + Empty, +} + +impl DocType { + /// Get a list of `UUIDv4` document types. + #[must_use] + pub fn doc_types(&self) -> &Vec { + &self.0 + } +} + +impl From for DocType { + fn from(value: UuidV4) -> Self { + DocType(vec![value]) + } +} + +impl TryFrom for DocType { + type Error = DocTypeError; + + fn try_from(value: Uuid) -> Result { + let uuid_v4 = UuidV4::try_from(value).map_err(|_| DocTypeError::InvalidUuid(value))?; + Ok(DocType(vec![uuid_v4])) + } +} + +impl TryFrom> for DocType { + type Error = DocTypeError; + + fn try_from(value: Vec) -> Result { + if value.is_empty() { + return Err(DocTypeError::Empty); + } + + let converted = value + .into_iter() + .map(|u| UuidV4::try_from(u).map_err(|_| DocTypeError::InvalidUuid(u))) + .collect::, DocTypeError>>()?; + + DocType::try_from(converted) + } +} + +impl TryFrom> for DocType { + type Error = DocTypeError; + + fn try_from(value: Vec) -> Result { + if value.is_empty() { + return Err(DocTypeError::Empty); + } + Ok(DocType(value)) + } +} + +impl Display for DocType { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "[{}]", + self.0 + .iter() + .map(UuidV4::to_string) + .collect::>() + .join(", ") + ) + } +} + +// ; Document Type +// document_type = [ 1* uuid_v4 ] +// ; UUIDv4 +// uuid_v4 = #6.37(bytes .size 16) +impl Decode<'_, DecodeContext<'_>> for DocType { + fn decode( + d: &mut Decoder, decode_context: &mut DecodeContext, + ) -> Result { + const CONTEXT: &str = "DocType decoding"; + let parse_uuid = |d: &mut Decoder| UuidV4::decode(d, &mut CborContext::Tagged); + + match d.datatype()? { + minicbor::data::Type::Array => { + let len = d.array()?.ok_or_else(|| { + decode_context + .report + .other("Unable to decode array length", CONTEXT); + minicbor::decode::Error::message(format!( + "{CONTEXT}: Unable to decode array length" + )) + })?; + + if len == 0 { + decode_context.report.invalid_value( + "array length", + "0", + "must contain at least one UUIDv4", + CONTEXT, + ); + return Err(minicbor::decode::Error::message(format!( + "{CONTEXT}: empty array" + ))); + } + + (0..len) + .map(|_| parse_uuid(d)) + .collect::, _>>() + .map(Self) + .map_err(|e| { + decode_context + .report + .other(&format!("Invalid UUIDv4 in array: {e}"), CONTEXT); + minicbor::decode::Error::message(format!( + "{CONTEXT}: Invalid UUIDv4 in array: {e}" + )) + }) + }, + minicbor::data::Type::Tag => { + // Handle single tagged UUID + match decode_context.compatibility_policy { + CompatibilityPolicy::Accept | CompatibilityPolicy::Warn => { + if matches!( + decode_context.compatibility_policy, + CompatibilityPolicy::Warn + ) { + warn!("{CONTEXT}: Conversion of document type single UUID to type DocType"); + } + + let uuid = parse_uuid(d).map_err(|e| { + let msg = format!("Cannot decode single UUIDv4: {e}"); + decode_context.report.invalid_value( + "Decode single UUIDv4", + &e.to_string(), + &msg, + CONTEXT, + ); + minicbor::decode::Error::message(format!("{CONTEXT}: {msg}")) + })?; + + let ids = map_doc_type(uuid.into()).map_err(|e| { + decode_context.report.other(&e.to_string(), CONTEXT); + minicbor::decode::Error::message(format!("{CONTEXT}: {e}")) + })?; + + let doc_type = ids.to_vec().try_into().map_err(|e: DocTypeError| { + decode_context.report.other(&e.to_string(), CONTEXT); + minicbor::decode::Error::message(format!("{CONTEXT}: {e}")) + })?; + + Ok(doc_type) + }, + + CompatibilityPolicy::Fail => { + let msg = "Conversion of document type single UUID to type DocType is not allowed"; + decode_context.report.other(msg, CONTEXT); + Err(minicbor::decode::Error::message(format!( + "{CONTEXT}: {msg}" + ))) + }, + } + }, + other => { + decode_context.report.invalid_value( + "decoding type", + &format!("{other:?}"), + "array or tag cbor", + CONTEXT, + ); + Err(minicbor::decode::Error::message(format!( + "{CONTEXT}: expected array of UUIDor tagged UUIDv4, got {other:?}", + ))) + }, + } + } +} + +/// Map single UUID doc type to new list of doc types +/// +fn map_doc_type(uuid: Uuid) -> anyhow::Result<&'static [Uuid]> { + const PROPOSAL_DOC: &[Uuid] = &[PROPOSAL_DOCUMENT_UUID_TYPE]; + const PROPOSAL_COMMENT_DOC: &[Uuid] = + &[COMMENT_DOCUMENT_UUID_TYPE, PROPOSAL_DOCUMENT_UUID_TYPE]; + const PROPOSAL_ACTION_DOC: &[Uuid] = &[ + PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + PROPOSAL_DOCUMENT_UUID_TYPE, + SUBMISSION_ACTION, + ]; + + match uuid { + id if id == PROPOSAL_DOCUMENT_UUID_TYPE => Ok(PROPOSAL_DOC), + id if id == COMMENT_DOCUMENT_UUID_TYPE => Ok(PROPOSAL_COMMENT_DOC), + id if id == PROPOSAL_ACTION_DOCUMENT_UUID_TYPE => Ok(PROPOSAL_ACTION_DOC), + _ => anyhow::bail!("Unknown document type: {uuid}"), + } +} + +impl Encode for DocType { + fn encode( + &self, e: &mut minicbor::Encoder, report: &mut ProblemReport, + ) -> Result<(), minicbor::encode::Error> { + const CONTEXT: &str = "DocType encoding"; + if self.0.is_empty() { + report.invalid_value("DocType", "empty", "DocType cannot be empty", CONTEXT); + return Err(minicbor::encode::Error::message(format!( + "{CONTEXT}: DocType cannot be empty" + ))); + } + + e.array(self.0.len().try_into().map_err(|_| { + report.other("Unable to encode array length", CONTEXT); + minicbor::encode::Error::message(format!("{CONTEXT}, unable to encode array length")) + })?)?; + + for id in &self.0 { + id.encode(e, &mut CborContext::Tagged).map_err(|_| { + report.other("Failed to encode UUIDv4", CONTEXT); + minicbor::encode::Error::message(format!("{CONTEXT}: UUIDv4 encoding failed")) + })?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use minicbor::Encoder; + + use super::*; + + // + // Proposal Submission Action = 37(h'5e60e623ad024a1ba1ac406db978ee48') should map to + // [37(h'5e60e623ad024a1ba1ac406db978ee48'), 37(h'7808d2bad51140af84e8c0d1625fdfdc'), + // 37(h'78927329cfd94ea19c710e019b126a65')] + const PSA: &str = "D825505E60E623AD024A1BA1AC406DB978EE48"; + + #[test] + fn test_empty_doc_type() { + assert!(>>::try_from(vec![]).is_err()); + + let mut report = ProblemReport::new("Test empty doc type"); + let mut decoded_context = DecodeContext { + compatibility_policy: CompatibilityPolicy::Accept, + report: &mut report, + }; + let mut decoder = Decoder::new(&[]); + assert!(DocType::decode(&mut decoder, &mut decoded_context).is_err()); + } + + #[test] + fn test_single_uuid_doc_type_fail_policy() { + let mut report = ProblemReport::new("Test single uuid doc type - fail"); + let data = hex::decode(PSA).unwrap(); + let decoder = Decoder::new(&data); + let mut decoded_context = DecodeContext { + compatibility_policy: CompatibilityPolicy::Fail, + report: &mut report, + }; + assert!(DocType::decode(&mut decoder.clone(), &mut decoded_context).is_err()); + } + + #[test] + fn test_single_uuid_doc_type_warn_policy() { + let mut report = ProblemReport::new("Test single uuid doc type - warn"); + let data = hex::decode(PSA).unwrap(); + let decoder = Decoder::new(&data); + let mut decoded_context = DecodeContext { + compatibility_policy: CompatibilityPolicy::Warn, + report: &mut report, + }; + let decoded_doc_type = DocType::decode(&mut decoder.clone(), &mut decoded_context).unwrap(); + assert_eq!(decoded_doc_type.doc_types().len(), 3); + } + + #[test] + fn test_single_uuid_doc_type_accept_policy() { + let mut report = ProblemReport::new("Test single uuid doc type - accept"); + let data = hex::decode(PSA).unwrap(); + let decoder = Decoder::new(&data); + let mut decoded_context = DecodeContext { + compatibility_policy: CompatibilityPolicy::Accept, + report: &mut report, + }; + let decoded_doc_type = DocType::decode(&mut decoder.clone(), &mut decoded_context).unwrap(); + assert_eq!(decoded_doc_type.doc_types().len(), 3); + } + + #[test] + fn test_multi_uuid_doc_type() { + let uuidv4 = UuidV4::new(); + let mut report = ProblemReport::new("Test multi uuid doc type"); + let doc_type_list: DocType = vec![uuidv4, uuidv4].try_into().unwrap(); + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + doc_type_list.encode(&mut encoder, &mut report).unwrap(); + let mut decoder = Decoder::new(&buffer); + let mut decoded_context = DecodeContext { + compatibility_policy: CompatibilityPolicy::Accept, + report: &mut report.clone(), + }; + let decoded_doc_type = DocType::decode(&mut decoder, &mut decoded_context).unwrap(); + assert_eq!(decoded_doc_type, doc_type_list); + } +} diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index bbbdb1677d7..8597f8ba7fa 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -3,6 +3,7 @@ use std::fmt::{Display, Formatter}; mod content_encoding; mod content_type; +mod doc_type; mod document_ref; mod extra_fields; mod section; @@ -15,6 +16,7 @@ use catalyst_types::{ pub use content_encoding::ContentEncoding; pub use content_type::ContentType; use coset::{cbor::Value, iana::CoapContentFormat}; +pub use doc_type::DocType; pub use document_ref::DocumentRef; pub use extra_fields::ExtraFields; pub use section::Section; From 60ff5be8781eb996a6d94a720e1c2396295235fd Mon Sep 17 00:00:00 2001 From: Alex Pozhylenkov Date: Tue, 27 May 2025 22:37:00 +0900 Subject: [PATCH 04/14] feat(rust/signed-doc): Add initial decoding tests for the Catalyst Signed Documents (#349) * wip * wip * fix fmt * fix spelling * fix clippy --- rust/signed_doc/src/metadata/extra_fields.rs | 4 +- rust/signed_doc/tests/decoding.rs | 158 +++++++++++++++---- 2 files changed, 132 insertions(+), 30 deletions(-) diff --git a/rust/signed_doc/src/metadata/extra_fields.rs b/rust/signed_doc/src/metadata/extra_fields.rs index 5decc4a7849..855e0a18323 100644 --- a/rust/signed_doc/src/metadata/extra_fields.rs +++ b/rust/signed_doc/src/metadata/extra_fields.rs @@ -79,8 +79,8 @@ impl ExtraFields { /// Return `collabs` field. #[must_use] - pub fn collabs(&self) -> &Vec { - &self.collabs + pub fn collabs(&self) -> &[String] { + self.collabs.as_slice() } /// Return `parameters` field. diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index c1f632f84aa..18697703c29 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -8,34 +8,6 @@ use ed25519_dalek::ed25519::signature::Signer; mod common; -#[test] -fn catalyst_signed_doc_cbor_roundtrip_test() { - let (uuid_v7, uuid_v4, metadata_fields) = common::test_metadata(); - let (sk, _, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); - - let content = serde_json::to_vec(&serde_json::Value::Null).unwrap(); - - let doc = Builder::new() - .with_json_metadata(metadata_fields.clone()) - .unwrap() - .with_decoded_content(content.clone()) - .add_signature(|m| sk.sign(&m).to_vec(), &kid) - .unwrap() - .build(); - - assert!(!doc.problem_report().is_problematic()); - - let bytes: Vec = doc.try_into().unwrap(); - let decoded: CatalystSignedDocument = bytes.as_slice().try_into().unwrap(); - let extra_fields: ExtraFields = serde_json::from_value(metadata_fields).unwrap(); - - assert_eq!(decoded.doc_type().unwrap(), uuid_v4); - assert_eq!(decoded.doc_id().unwrap(), uuid_v7); - assert_eq!(decoded.doc_ver().unwrap(), uuid_v7); - assert_eq!(decoded.doc_content().decoded_bytes().unwrap(), &content); - assert_eq!(decoded.doc_meta(), &extra_fields); -} - #[test] fn catalyst_signed_doc_cbor_roundtrip_kid_as_id_test() { let (_, _, metadata_fields) = common::test_metadata(); @@ -181,3 +153,133 @@ async fn catalyst_signed_doc_parameters_aliases_test() { .unwrap(); assert!(doc.problem_report().is_problematic()); } + +type PostCheck = dyn Fn(&CatalystSignedDocument) -> bool; + +struct TestCase { + name: &'static str, + bytes_gen: Box Vec>, + // If the provided bytes can be even decoded without error (valid COSE or not). + // If set to `false` all further checks will not even happen. + can_decode: bool, + // If the decoded doc is a valid `CatalystSignedDocument`, underlying problem report is empty. + valid_doc: bool, + post_checks: Option>, +} + +fn decoding_empty_bytes_case() -> TestCase { + TestCase { + name: "Decoding empty bytes", + bytes_gen: Box::new(Vec::new), + can_decode: false, + valid_doc: false, + post_checks: None, + } +} + +#[allow(clippy::unwrap_used)] +fn signed_doc_with_all_fields_case() -> TestCase { + let uuid_v7 = UuidV7::new(); + let uuid_v4 = UuidV4::new(); + let (sk, _, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); + + TestCase { + name: "Catalyst Signed Doc with ALL defined metadata fields and signatures", + bytes_gen: Box::new({ + let kid = kid.clone(); + move || { + Builder::new() + .with_json_metadata(serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + "type": uuid_v4.to_string(), + "id": uuid_v7.to_string(), + "ver": uuid_v7.to_string(), + "ref": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, + "reply": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, + "template": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, + "section": "$".to_string(), + "collabs": vec!["Alex1".to_string(), "Alex2".to_string()], + "parameters": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, + })) + .unwrap() + .with_decoded_content(serde_json::to_vec(&serde_json::Value::Null).unwrap()) + .add_signature(|m| sk.sign(&m).to_vec(), &kid) + .unwrap() + .build() + .try_into() + .unwrap() + } + }), + can_decode: true, + valid_doc: true, + post_checks: Some(Box::new({ + move |doc| { + (doc.doc_type().unwrap() == uuid_v4) + && (doc.doc_id().unwrap() == uuid_v7) + && (doc.doc_ver().unwrap() == uuid_v7) + && (doc.doc_content_type().unwrap() == ContentType::Json) + && (doc.doc_content_encoding().unwrap() == ContentEncoding::Brotli) + && (doc.doc_meta().doc_ref().unwrap() + == DocumentRef { + id: uuid_v7, + ver: uuid_v7, + }) + && (doc.doc_meta().reply().unwrap() + == DocumentRef { + id: uuid_v7, + ver: uuid_v7, + }) + && (doc.doc_meta().template().unwrap() + == DocumentRef { + id: uuid_v7, + ver: uuid_v7, + }) + && (doc.doc_meta().parameters().unwrap() + == DocumentRef { + id: uuid_v7, + ver: uuid_v7, + }) + && (doc.doc_meta().section().unwrap() == &"$".parse::
().unwrap()) + && (doc.doc_meta().collabs() == ["Alex1".to_string(), "Alex2".to_string()]) + && (doc.doc_content().decoded_bytes().unwrap() + == serde_json::to_vec(&serde_json::Value::Null).unwrap()) + && (doc.kids() == vec![kid.clone()]) + } + })), + } +} + +#[test] +fn catalyst_signed_doc_decoding_test() { + let test_cases = [ + decoding_empty_bytes_case(), + signed_doc_with_all_fields_case(), + ]; + + for case in test_cases { + let bytes = case.bytes_gen.as_ref()(); + let doc_res = CatalystSignedDocument::try_from(bytes.as_slice()); + assert_eq!(doc_res.is_ok(), case.can_decode, "Case: [{}]", case.name); + if let Ok(doc) = doc_res { + assert_eq!( + !doc.problem_report().is_problematic(), + case.valid_doc, + "Case: [{}]. Problem report: {:?}", + case.name, + doc.problem_report() + ); + + if let Some(post_checks) = &case.post_checks { + assert!((post_checks.as_ref())(&doc), "Case: [{}]", case.name); + } + + assert_eq!( + bytes, + Vec::::try_from(doc).unwrap(), + "Case: [{}]. Asymmetric encoding and decoding procedure", + case.name + ); + } + } +} From b5f12bb005418bb8a2b1735b81539a6282ed9c11 Mon Sep 17 00:00:00 2001 From: bkioshn <35752733+bkioshn@users.noreply.github.com> Date: Thu, 29 May 2025 13:37:38 +0700 Subject: [PATCH 05/14] fix(rust/signed-doc): Apply new `DocType` (#347) * feat(signed-doc): add new type DocType Signed-off-by: bkioshn * wip: apply doctype Signed-off-by: bkioshn * fix(signed-doc): add more function to DocType Signed-off-by: bkioshn * fix(signed-doc): map old doctype to new doctype Signed-off-by: bkioshn * fix(signed-doc): add eq to uuidv4 Signed-off-by: bkioshn * fix(signed-doc): fix validator Signed-off-by: bkioshn * fix(signed-doc): minor fixes Signed-off-by: bkioshn * fix(catalyst-types): add hash to uuidv4 Signed-off-by: bkioshn * fix(signed-doc): decoding test Signed-off-by: bkioshn * fix(signed-doc): doctype Signed-off-by: bkioshn * fix(signed-doc): minor fixes Signed-off-by: bkioshn * chore(sign-doc): fix comment Signed-off-by: bkioshn * fix(catalyst-types): add fromstr to uuidv4 and 7 Signed-off-by: bkioshn * fix(signed-doc): restructure doctypes Signed-off-by: bkioshn * fix(signed-doc): test Signed-off-by: bkioshn * Update rust/signed_doc/tests/proposal.rs * Update rust/signed_doc/tests/comment.rs * Update rust/signed_doc/tests/submission.rs --------- Signed-off-by: bkioshn Co-authored-by: Alex Pozhylenkov --- rust/catalyst-types/src/uuid/mod.rs | 6 +- rust/catalyst-types/src/uuid/uuid_v4.rs | 16 +- rust/catalyst-types/src/uuid/uuid_v7.rs | 14 +- rust/signed_doc/src/doc_types/mod.rs | 153 +++++++---- rust/signed_doc/src/lib.rs | 13 +- rust/signed_doc/src/metadata/doc_type.rs | 238 +++++++++++++++--- rust/signed_doc/src/metadata/mod.rs | 62 ++--- rust/signed_doc/src/validator/mod.rs | 51 ++-- .../signed_doc/src/validator/rules/doc_ref.rs | 26 +- .../src/validator/rules/parameters.rs | 16 +- rust/signed_doc/src/validator/rules/reply.rs | 21 +- .../src/validator/rules/template.rs | 36 +-- rust/signed_doc/tests/comment.rs | 60 ++++- rust/signed_doc/tests/common/mod.rs | 23 ++ rust/signed_doc/tests/decoding.rs | 27 +- rust/signed_doc/tests/proposal.rs | 40 ++- rust/signed_doc/tests/submission.rs | 49 +++- 17 files changed, 629 insertions(+), 222 deletions(-) diff --git a/rust/catalyst-types/src/uuid/mod.rs b/rust/catalyst-types/src/uuid/mod.rs index 3e25737e1de..c2df1c4795c 100644 --- a/rust/catalyst-types/src/uuid/mod.rs +++ b/rust/catalyst-types/src/uuid/mod.rs @@ -15,8 +15,7 @@ use minicbor::data::Tag; pub const INVALID_UUID: uuid::Uuid = uuid::Uuid::from_bytes([0x00; 16]); /// UUID CBOR tag . -#[allow(dead_code)] -const UUID_CBOR_TAG: u64 = 37; +pub const UUID_CBOR_TAG: u64 = 37; /// Uuid validation errors, which could occur during decoding or converting to /// `UuidV4` or `UuidV7` types. @@ -29,6 +28,9 @@ pub enum UuidError { /// `UUIDv7` invalid error #[error("'{0}' is not a valid UUIDv7")] InvalidUuidV7(uuid::Uuid), + /// Invalid string conversion + #[error("Invalid string conversion: {0}")] + StringConversion(String), } /// Context for `CBOR` encoding and decoding diff --git a/rust/catalyst-types/src/uuid/uuid_v4.rs b/rust/catalyst-types/src/uuid/uuid_v4.rs index c7e2dbb8149..a7baf462484 100644 --- a/rust/catalyst-types/src/uuid/uuid_v4.rs +++ b/rust/catalyst-types/src/uuid/uuid_v4.rs @@ -1,5 +1,8 @@ //! `UUIDv4` Type. -use std::fmt::{Display, Formatter}; +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; use minicbor::{Decode, Decoder, Encode}; use uuid::Uuid; @@ -7,7 +10,7 @@ use uuid::Uuid; use super::{decode_cbor_uuid, encode_cbor_uuid, CborContext, UuidError, INVALID_UUID}; /// Type representing a `UUIDv4`. -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Serialize)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, serde::Serialize)] pub struct UuidV4(Uuid); impl UuidV4 { @@ -106,6 +109,15 @@ impl<'de> serde::Deserialize<'de> for UuidV4 { } } +impl FromStr for UuidV4 { + type Err = UuidError; + + fn from_str(s: &str) -> Result { + let uuid = Uuid::parse_str(s).map_err(|_| UuidError::StringConversion(s.to_string()))?; + UuidV4::try_from(uuid).map_err(|_| UuidError::InvalidUuidV4(uuid)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/catalyst-types/src/uuid/uuid_v7.rs b/rust/catalyst-types/src/uuid/uuid_v7.rs index 98fbd8cda6e..1bb95e6ff90 100644 --- a/rust/catalyst-types/src/uuid/uuid_v7.rs +++ b/rust/catalyst-types/src/uuid/uuid_v7.rs @@ -1,5 +1,8 @@ //! `UUIDv7` Type. -use std::fmt::{Display, Formatter}; +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; use minicbor::{Decode, Decoder, Encode}; use uuid::Uuid; @@ -106,6 +109,15 @@ impl<'de> serde::Deserialize<'de> for UuidV7 { } } +impl FromStr for UuidV7 { + type Err = UuidError; + + fn from_str(s: &str) -> Result { + let uuid = Uuid::parse_str(s).map_err(|_| UuidError::StringConversion(s.to_string()))?; + UuidV7::try_from(uuid).map_err(|_| UuidError::InvalidUuidV7(uuid)) + } +} + #[cfg(test)] mod tests { use uuid::Uuid; diff --git a/rust/signed_doc/src/doc_types/mod.rs b/rust/signed_doc/src/doc_types/mod.rs index 15bcb5948f5..2a2c22735e4 100644 --- a/rust/signed_doc/src/doc_types/mod.rs +++ b/rust/signed_doc/src/doc_types/mod.rs @@ -1,55 +1,108 @@ //! An implementation of different defined document types //! +use std::sync::LazyLock; + use catalyst_types::uuid::Uuid; +use deprecated::{ + COMMENT_DOCUMENT_UUID_TYPE, PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, PROPOSAL_DOCUMENT_UUID_TYPE, +}; + +use crate::DocType; + +/// Proposal document type. +#[allow(clippy::expect_used)] +pub static PROPOSAL_DOC_TYPE: LazyLock = LazyLock::new(|| { + let ids = &[PROPOSAL_UUID_TYPE]; + ids.to_vec() + .try_into() + .expect("Failed to convert proposal document Uuid to DocType") +}); + +/// Proposal comment document type. +#[allow(clippy::expect_used)] +pub static PROPOSAL_COMMENT_DOC: LazyLock = LazyLock::new(|| { + let ids = &[COMMENT_UUID_TYPE, PROPOSAL_UUID_TYPE]; + ids.to_vec() + .try_into() + .expect("Failed to convert proposal comment document Uuid to DocType") +}); + +/// Proposal action document type. +#[allow(clippy::expect_used)] +pub static PROPOSAL_ACTION_DOC: LazyLock = LazyLock::new(|| { + let ids = &[ + ACTION_UUID_TYPE, + PROPOSAL_UUID_TYPE, + SUBMISSION_ACTION_UUID_TYPE, + ]; + ids.to_vec() + .try_into() + .expect("Failed to convert proposal action document Uuid to DocType") +}); + +/// Submission Action UUID type. +pub const SUBMISSION_ACTION_UUID_TYPE: Uuid = + Uuid::from_u128(0x7892_7329_CFD9_4EA1_9C71_0E01_9B12_6A65); +/// Proposal UUID type. +pub const PROPOSAL_UUID_TYPE: Uuid = PROPOSAL_DOCUMENT_UUID_TYPE; +/// Comment UUID type. +pub const COMMENT_UUID_TYPE: Uuid = COMMENT_DOCUMENT_UUID_TYPE; +/// Action UUID type. +pub const ACTION_UUID_TYPE: Uuid = PROPOSAL_ACTION_DOCUMENT_UUID_TYPE; + +/// Document type which will be deprecated. +pub mod deprecated { + use catalyst_types::uuid::Uuid; -/// Proposal document `UuidV4` type. -pub const PROPOSAL_DOCUMENT_UUID_TYPE: Uuid = - Uuid::from_u128(0x7808_D2BA_D511_40AF_84E8_C0D1_625F_DFDC); -/// Proposal template `UuidV4` type. -pub const PROPOSAL_TEMPLATE_UUID_TYPE: Uuid = - Uuid::from_u128(0x0CE8_AB38_9258_4FBC_A62E_7FAA_6E58_318F); -/// Comment document `UuidV4` type. -pub const COMMENT_DOCUMENT_UUID_TYPE: Uuid = - Uuid::from_u128(0xB679_DED3_0E7C_41BA_89F8_DA62_A178_98EA); -/// Comment template `UuidV4` type. -pub const COMMENT_TEMPLATE_UUID_TYPE: Uuid = - Uuid::from_u128(0x0B84_24D4_EBFD_46E3_9577_1775_A69D_290C); -/// Review document `UuidV4` type. -pub const REVIEW_DOCUMENT_UUID_TYPE: Uuid = - Uuid::from_u128(0xE4CA_F5F0_098B_45FD_94F3_0702_A457_3DB5); -/// Review template `UuidV4` type. -pub const REVIEW_TEMPLATE_UUID_TYPE: Uuid = - Uuid::from_u128(0xEBE5_D0BF_5D86_4577_AF4D_008F_DDBE_2EDC); -/// Category document `UuidV4` type. -pub const CATEGORY_DOCUMENT_UUID_TYPE: Uuid = - Uuid::from_u128(0x48C2_0109_362A_4D32_9BBA_E0A9_CF8B_45BE); -/// Category template `UuidV4` type. -pub const CATEGORY_TEMPLATE_UUID_TYPE: Uuid = - Uuid::from_u128(0x65B1_E8B0_51F1_46A5_9970_72CD_F268_84BE); -/// Campaign parameters document `UuidV4` type. -pub const CAMPAIGN_DOCUMENT_UUID_TYPE: Uuid = - Uuid::from_u128(0x0110_EA96_A555_47CE_8408_36EF_E6ED_6F7C); -/// Campaign parameters template `UuidV4` type. -pub const CAMPAIGN_TEMPLATE_UUID_TYPE: Uuid = - Uuid::from_u128(0x7E8F_5FA2_44CE_49C8_BFD5_02AF_42C1_79A3); -/// Brand parameters document `UuidV4` type. -pub const BRAND_DOCUMENT_UUID_TYPE: Uuid = - Uuid::from_u128(0x3E48_08CC_C86E_467B_9702_D60B_AA9D_1FCA); -/// Brand parameters template `UuidV4` type. -pub const BRAND_TEMPLATE_UUID_TYPE: Uuid = - Uuid::from_u128(0xFD3C_1735_80B1_4EEA_8D63_5F43_6D97_EA31); -/// Proposal action document `UuidV4` type. -pub const PROPOSAL_ACTION_DOCUMENT_UUID_TYPE: Uuid = - Uuid::from_u128(0x5E60_E623_AD02_4A1B_A1AC_406D_B978_EE48); -/// Public vote transaction v2 `UuidV4` type. -pub const PUBLIC_VOTE_TX_V2_UUID_TYPE: Uuid = - Uuid::from_u128(0x8DE5_586C_E998_4B95_8742_7BE3_C859_2803); -/// Private vote transaction v2 `UuidV4` type. -pub const PRIVATE_VOTE_TX_V2_UUID_TYPE: Uuid = - Uuid::from_u128(0xE78E_E18D_F380_44C1_A852_80AA_6ECB_07FE); -/// Immutable ledger block `UuidV4` type. -pub const IMMUTABLE_LEDGER_BLOCK_UUID_TYPE: Uuid = - Uuid::from_u128(0xD9E7_E6CE_2401_4D7D_9492_F4F7_C642_41C3); -/// Submission Action `UuidV4` type. -pub const SUBMISSION_ACTION: Uuid = Uuid::from_u128(0x7892_7329_CFD9_4EA1_9C71_0E01_9B12_6A65); + /// Proposal document `UuidV4` type. + pub const PROPOSAL_DOCUMENT_UUID_TYPE: Uuid = + Uuid::from_u128(0x7808_D2BA_D511_40AF_84E8_C0D1_625F_DFDC); + /// Proposal template `UuidV4` type. + pub const PROPOSAL_TEMPLATE_UUID_TYPE: Uuid = + Uuid::from_u128(0x0CE8_AB38_9258_4FBC_A62E_7FAA_6E58_318F); + /// Comment document `UuidV4` type. + pub const COMMENT_DOCUMENT_UUID_TYPE: Uuid = + Uuid::from_u128(0xB679_DED3_0E7C_41BA_89F8_DA62_A178_98EA); + /// Comment template `UuidV4` type. + pub const COMMENT_TEMPLATE_UUID_TYPE: Uuid = + Uuid::from_u128(0x0B84_24D4_EBFD_46E3_9577_1775_A69D_290C); + /// Review document `UuidV4` type. + pub const REVIEW_DOCUMENT_UUID_TYPE: Uuid = + Uuid::from_u128(0xE4CA_F5F0_098B_45FD_94F3_0702_A457_3DB5); + /// Review template `UuidV4` type. + pub const REVIEW_TEMPLATE_UUID_TYPE: Uuid = + Uuid::from_u128(0xEBE5_D0BF_5D86_4577_AF4D_008F_DDBE_2EDC); + /// Category document `UuidV4` type. + pub const CATEGORY_DOCUMENT_UUID_TYPE: Uuid = + Uuid::from_u128(0x48C2_0109_362A_4D32_9BBA_E0A9_CF8B_45BE); + /// Category template `UuidV4` type. + pub const CATEGORY_TEMPLATE_UUID_TYPE: Uuid = + Uuid::from_u128(0x65B1_E8B0_51F1_46A5_9970_72CD_F268_84BE); + /// Campaign parameters document `UuidV4` type. + pub const CAMPAIGN_DOCUMENT_UUID_TYPE: Uuid = + Uuid::from_u128(0x0110_EA96_A555_47CE_8408_36EF_E6ED_6F7C); + /// Campaign parameters template `UuidV4` type. + pub const CAMPAIGN_TEMPLATE_UUID_TYPE: Uuid = + Uuid::from_u128(0x7E8F_5FA2_44CE_49C8_BFD5_02AF_42C1_79A3); + /// Brand parameters document `UuidV4` type. + pub const BRAND_DOCUMENT_UUID_TYPE: Uuid = + Uuid::from_u128(0x3E48_08CC_C86E_467B_9702_D60B_AA9D_1FCA); + /// Brand parameters template `UuidV4` type. + pub const BRAND_TEMPLATE_UUID_TYPE: Uuid = + Uuid::from_u128(0xFD3C_1735_80B1_4EEA_8D63_5F43_6D97_EA31); + /// Proposal action document `UuidV4` type. + pub const PROPOSAL_ACTION_DOCUMENT_UUID_TYPE: Uuid = + Uuid::from_u128(0x5E60_E623_AD02_4A1B_A1AC_406D_B978_EE48); + /// Public vote transaction v2 `UuidV4` type. + pub const PUBLIC_VOTE_TX_V2_UUID_TYPE: Uuid = + Uuid::from_u128(0x8DE5_586C_E998_4B95_8742_7BE3_C859_2803); + /// Private vote transaction v2 `UuidV4` type. + pub const PRIVATE_VOTE_TX_V2_UUID_TYPE: Uuid = + Uuid::from_u128(0xE78E_E18D_F380_44C1_A852_80AA_6ECB_07FE); + /// Immutable ledger block `UuidV4` type. + pub const IMMUTABLE_LEDGER_BLOCK_UUID_TYPE: Uuid = + Uuid::from_u128(0xD9E7_E6CE_2401_4D7D_9492_F4F7_C642_41C3); + /// Submission Action `UuidV4` type. + pub const SUBMISSION_ACTION: Uuid = Uuid::from_u128(0x7892_7329_CFD9_4EA1_9C71_0E01_9B12_6A65); +} diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 75e0681a48a..b7e599ef042 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -23,6 +23,7 @@ pub use catalyst_types::{ }; pub use content::Content; use coset::{CborSerializable, Header, TaggedCborSerializable}; +use decode_context::{CompatibilityPolicy, DecodeContext}; pub use metadata::{ ContentEncoding, ContentType, DocType, DocumentRef, ExtraFields, Metadata, Section, }; @@ -88,11 +89,11 @@ impl From for CatalystSignedDocument { impl CatalystSignedDocument { // A bunch of getters to access the contents, or reason through the document, such as. - /// Return Document Type `UUIDv4`. + /// Return Document Type `DocType` - List of `UUIDv4`. /// /// # Errors /// - Missing 'type' field. - pub fn doc_type(&self) -> anyhow::Result { + pub fn doc_type(&self) -> anyhow::Result<&DocType> { self.inner.metadata.doc_type() } @@ -238,8 +239,12 @@ impl Decode<'_, ()> for CatalystSignedDocument { minicbor::decode::Error::message(format!("Invalid COSE Sign document: {e}")) })?; - let report = ProblemReport::new(PROBLEM_REPORT_CTX); - let metadata = Metadata::from_protected_header(&cose_sign.protected, &report); + let mut report = ProblemReport::new(PROBLEM_REPORT_CTX); + let mut ctx = DecodeContext { + compatibility_policy: CompatibilityPolicy::Accept, + report: &mut report, + }; + let metadata = Metadata::from_protected_header(&cose_sign.protected, &mut ctx); let signatures = Signatures::from_cose_sig_list(&cose_sign.signatures, &report); let content = if let Some(payload) = cose_sign.payload { diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs index c26240df030..a7c2ca205bb 100644 --- a/rust/signed_doc/src/metadata/doc_type.rs +++ b/rust/signed_doc/src/metadata/doc_type.rs @@ -1,24 +1,29 @@ //! Document Type. -use std::fmt::{Display, Formatter}; +use std::{ + fmt::{Display, Formatter}, + hash::{Hash, Hasher}, +}; use catalyst_types::{ problem_report::ProblemReport, - uuid::{CborContext, Uuid, UuidV4}, + uuid::{CborContext, Uuid, UuidV4, UUID_CBOR_TAG}, }; +use coset::cbor::Value; use minicbor::{Decode, Decoder, Encode}; +use serde::{Deserialize, Deserializer}; use tracing::warn; use crate::{ decode_context::{CompatibilityPolicy, DecodeContext}, doc_types::{ - COMMENT_DOCUMENT_UUID_TYPE, PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, - PROPOSAL_DOCUMENT_UUID_TYPE, SUBMISSION_ACTION, + ACTION_UUID_TYPE, COMMENT_UUID_TYPE, PROPOSAL_ACTION_DOC, PROPOSAL_COMMENT_DOC, + PROPOSAL_DOC_TYPE, PROPOSAL_UUID_TYPE, }, }; /// List of `UUIDv4` document type. -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, Eq)] pub struct DocType(Vec); /// `DocType` Errors. @@ -30,6 +35,9 @@ pub enum DocTypeError { /// `DocType` cannot be empty. #[error("DocType cannot be empty")] Empty, + /// Invalid string conversion + #[error("Invalid string conversion: {0}")] + StringConversion(String), } impl DocType { @@ -38,6 +46,32 @@ impl DocType { pub fn doc_types(&self) -> &Vec { &self.0 } + + /// Convert `DocType` to coset `Value`. + pub(crate) fn to_value(&self) -> Value { + Value::Array( + self.0 + .iter() + .map(|uuidv4| { + Value::Tag( + UUID_CBOR_TAG, + Box::new(Value::Bytes(uuidv4.uuid().as_bytes().to_vec())), + ) + }) + .collect(), + ) + } +} + +impl Hash for DocType { + fn hash(&self, state: &mut H) { + let list = self + .0 + .iter() + .map(std::string::ToString::to_string) + .collect::>(); + list.hash(state); + } } impl From for DocType { @@ -83,6 +117,25 @@ impl TryFrom> for DocType { } } +impl TryFrom> for DocType { + type Error = DocTypeError; + + fn try_from(value: Vec) -> Result { + if value.is_empty() { + return Err(DocTypeError::Empty); + } + let converted = value + .into_iter() + .map(|s| { + s.parse::() + .map_err(|_| DocTypeError::StringConversion(s)) + }) + .collect::, _>>()?; + + Ok(DocType(converted)) + } +} + impl Display for DocType { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { write!( @@ -166,12 +219,7 @@ impl Decode<'_, DecodeContext<'_>> for DocType { minicbor::decode::Error::message(format!("{CONTEXT}: {msg}")) })?; - let ids = map_doc_type(uuid.into()).map_err(|e| { - decode_context.report.other(&e.to_string(), CONTEXT); - minicbor::decode::Error::message(format!("{CONTEXT}: {e}")) - })?; - - let doc_type = ids.to_vec().try_into().map_err(|e: DocTypeError| { + let doc_type = map_doc_type(uuid.into()).map_err(|e| { decode_context.report.other(&e.to_string(), CONTEXT); minicbor::decode::Error::message(format!("{CONTEXT}: {e}")) })?; @@ -205,20 +253,11 @@ impl Decode<'_, DecodeContext<'_>> for DocType { /// Map single UUID doc type to new list of doc types /// -fn map_doc_type(uuid: Uuid) -> anyhow::Result<&'static [Uuid]> { - const PROPOSAL_DOC: &[Uuid] = &[PROPOSAL_DOCUMENT_UUID_TYPE]; - const PROPOSAL_COMMENT_DOC: &[Uuid] = - &[COMMENT_DOCUMENT_UUID_TYPE, PROPOSAL_DOCUMENT_UUID_TYPE]; - const PROPOSAL_ACTION_DOC: &[Uuid] = &[ - PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, - PROPOSAL_DOCUMENT_UUID_TYPE, - SUBMISSION_ACTION, - ]; - +fn map_doc_type(uuid: Uuid) -> anyhow::Result { match uuid { - id if id == PROPOSAL_DOCUMENT_UUID_TYPE => Ok(PROPOSAL_DOC), - id if id == COMMENT_DOCUMENT_UUID_TYPE => Ok(PROPOSAL_COMMENT_DOC), - id if id == PROPOSAL_ACTION_DOCUMENT_UUID_TYPE => Ok(PROPOSAL_ACTION_DOC), + id if id == PROPOSAL_UUID_TYPE => Ok(PROPOSAL_DOC_TYPE.clone()), + id if id == COMMENT_UUID_TYPE => Ok(PROPOSAL_COMMENT_DOC.clone()), + id if id == ACTION_UUID_TYPE => Ok(PROPOSAL_ACTION_DOC.clone()), _ => anyhow::bail!("Unknown document type: {uuid}"), } } @@ -250,10 +289,65 @@ impl Encode for DocType { } } +impl<'de> Deserialize<'de> for DocType { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + #[derive(Deserialize)] + #[serde(untagged)] + enum DocTypeInput { + /// Single UUID string. + Single(String), + /// List of UUID string. + Multiple(Vec), + } + + let input = DocTypeInput::deserialize(deserializer)?; + let dt = match input { + DocTypeInput::Single(s) => { + let uuid = Uuid::parse_str(&s).map_err(|_| { + serde::de::Error::custom(DocTypeError::StringConversion(s.clone())) + })?; + // If there is a map from old (single uuid) to new use that list, else convert that + // single uuid to [uuid] - of type DocType + map_doc_type(uuid).unwrap_or(uuid.try_into().map_err(serde::de::Error::custom)?) + }, + DocTypeInput::Multiple(v) => v.try_into().map_err(serde::de::Error::custom)?, + }; + Ok(dt) + } +} + +// This is needed to preserve backward compatibility with the old solution. +impl PartialEq for DocType { + fn eq(&self, other: &Self) -> bool { + // List of special-case (single UUID) -> new DocType + // The old one should equal to the new one + let special_cases = [ + (PROPOSAL_UUID_TYPE, &*PROPOSAL_DOC_TYPE), + (COMMENT_UUID_TYPE, &*PROPOSAL_COMMENT_DOC), + (ACTION_UUID_TYPE, &*PROPOSAL_ACTION_DOC), + ]; + for (uuid, expected) in special_cases { + match DocType::try_from(uuid) { + Ok(single) => { + if (self.0 == single.0 && other.0 == expected.0) + || (other.0 == single.0 && self.0 == expected.0) + { + return true; + } + }, + Err(_) => return false, + } + } + self.0 == other.0 + } +} + #[cfg(test)] mod tests { use minicbor::Encoder; + use serde_json::json; use super::*; @@ -264,7 +358,7 @@ mod tests { const PSA: &str = "D825505E60E623AD024A1BA1AC406DB978EE48"; #[test] - fn test_empty_doc_type() { + fn test_empty_doc_type_cbor_decode() { assert!(>>::try_from(vec![]).is_err()); let mut report = ProblemReport::new("Test empty doc type"); @@ -277,7 +371,7 @@ mod tests { } #[test] - fn test_single_uuid_doc_type_fail_policy() { + fn test_single_uuid_doc_type_fail_policy_cbor_decode() { let mut report = ProblemReport::new("Test single uuid doc type - fail"); let data = hex::decode(PSA).unwrap(); let decoder = Decoder::new(&data); @@ -289,7 +383,7 @@ mod tests { } #[test] - fn test_single_uuid_doc_type_warn_policy() { + fn test_single_uuid_doc_type_warn_policy_cbor_decode() { let mut report = ProblemReport::new("Test single uuid doc type - warn"); let data = hex::decode(PSA).unwrap(); let decoder = Decoder::new(&data); @@ -302,7 +396,7 @@ mod tests { } #[test] - fn test_single_uuid_doc_type_accept_policy() { + fn test_single_uuid_doc_type_accept_policy_cbor_decode() { let mut report = ProblemReport::new("Test single uuid doc type - accept"); let data = hex::decode(PSA).unwrap(); let decoder = Decoder::new(&data); @@ -315,7 +409,7 @@ mod tests { } #[test] - fn test_multi_uuid_doc_type() { + fn test_multi_uuid_doc_type_cbor_decode_encode() { let uuidv4 = UuidV4::new(); let mut report = ProblemReport::new("Test multi uuid doc type"); let doc_type_list: DocType = vec![uuidv4, uuidv4].try_into().unwrap(); @@ -330,4 +424,90 @@ mod tests { let decoded_doc_type = DocType::decode(&mut decoder, &mut decoded_context).unwrap(); assert_eq!(decoded_doc_type, doc_type_list); } + + #[test] + fn test_valid_vec_string() { + let uuid = Uuid::new_v4().to_string(); + let input = vec![uuid.clone()]; + let doc_type = DocType::try_from(input).expect("should succeed"); + + assert_eq!(doc_type.0.len(), 1); + assert_eq!(doc_type.0.first().unwrap().to_string(), uuid); + } + + #[test] + fn test_empty_vec_string_fails() { + let input: Vec = vec![]; + let result = DocType::try_from(input); + assert!(matches!(result, Err(DocTypeError::Empty))); + } + + #[test] + fn test_invalid_uuid_vec_string() { + let input = vec!["not-a-uuid".to_string()]; + let result = DocType::try_from(input); + assert!(matches!(result, Err(DocTypeError::StringConversion(s)) if s == "not-a-uuid")); + } + + #[test] + fn test_doc_type_to_value() { + let uuid = uuid::Uuid::new_v4(); + let doc_type = DocType(vec![UuidV4::try_from(uuid).unwrap()]); + + for d in &doc_type.to_value().into_array().unwrap() { + let t = d.clone().into_tag().unwrap(); + assert_eq!(t.0, UUID_CBOR_TAG); + assert_eq!(t.1.as_bytes().unwrap().len(), 16); + } + } + + #[test] + fn test_doctype_equal_special_cases() { + // Direct equal + let uuid = PROPOSAL_UUID_TYPE; + let dt1 = DocType::try_from(vec![uuid]).unwrap(); + let dt2 = DocType::try_from(vec![uuid]).unwrap(); + assert_eq!(dt1, dt2); + + // single -> special mapped type + let single = DocType::try_from(PROPOSAL_UUID_TYPE).unwrap(); + assert_eq!(single, *PROPOSAL_DOC_TYPE); + let single = DocType::try_from(COMMENT_UUID_TYPE).unwrap(); + assert_eq!(single, *PROPOSAL_COMMENT_DOC); + let single = DocType::try_from(ACTION_UUID_TYPE).unwrap(); + assert_eq!(single, *PROPOSAL_ACTION_DOC); + } + + #[test] + fn test_deserialize_single_uuid_normal() { + let uuid = uuid::Uuid::new_v4().to_string(); + let json = json!(uuid); + let dt: DocType = serde_json::from_value(json).unwrap(); + + assert_eq!(dt.0.len(), 1); + assert_eq!(dt.0.first().unwrap().to_string(), uuid); + } + + #[test] + fn test_deserialize_multiple_uuids() { + let uuid1 = uuid::Uuid::new_v4().to_string(); + let uuid2 = uuid::Uuid::new_v4().to_string(); + let json = json!([uuid1.clone(), uuid2.clone()]); + + let dt: DocType = serde_json::from_value(json).unwrap(); + let actual = + dt.0.iter() + .map(std::string::ToString::to_string) + .collect::>(); + assert_eq!(actual, vec![uuid1, uuid2]); + } + + #[test] + fn test_deserialize_special_case() { + let uuid = PROPOSAL_UUID_TYPE.to_string(); + let json = json!(uuid); + let dt: DocType = serde_json::from_value(json).unwrap(); + + assert_eq!(dt, *PROPOSAL_DOC_TYPE); + } } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 8597f8ba7fa..703b71eacc1 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -3,26 +3,24 @@ use std::fmt::{Display, Formatter}; mod content_encoding; mod content_type; -mod doc_type; +pub(crate) mod doc_type; mod document_ref; mod extra_fields; mod section; pub(crate) mod utils; -use catalyst_types::{ - problem_report::ProblemReport, - uuid::{UuidV4, UuidV7}, -}; +use catalyst_types::{problem_report::ProblemReport, uuid::UuidV7}; pub use content_encoding::ContentEncoding; pub use content_type::ContentType; -use coset::{cbor::Value, iana::CoapContentFormat}; +use coset::{cbor::Value, iana::CoapContentFormat, CborSerializable}; pub use doc_type::DocType; pub use document_ref::DocumentRef; pub use extra_fields::ExtraFields; +use minicbor::{Decode, Decoder}; pub use section::Section; -use utils::{ - cose_protected_header_find, decode_document_field_from_protected_header, CborUuidV4, CborUuidV7, -}; +use utils::{cose_protected_header_find, decode_document_field_from_protected_header, CborUuidV7}; + +use crate::decode_context::DecodeContext; /// `content_encoding` field COSE key value const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; @@ -42,9 +40,9 @@ pub struct Metadata(InnerMetadata); /// An actual representation of all metadata fields. #[derive(Clone, Debug, PartialEq, serde::Deserialize, Default)] pub(crate) struct InnerMetadata { - /// Document Type `UUIDv4`. + /// Document Type, list of `UUIDv4`. #[serde(rename = "type")] - doc_type: Option, + doc_type: Option, /// Document ID `UUIDv7`. id: Option, /// Document Version `UUIDv7`. @@ -61,13 +59,14 @@ pub(crate) struct InnerMetadata { } impl Metadata { - /// Return Document Type `UUIDv4`. + /// Return Document Type `DocType` - a list of `UUIDv4`. /// /// # Errors /// - Missing 'type' field. - pub fn doc_type(&self) -> anyhow::Result { + pub fn doc_type(&self) -> anyhow::Result<&DocType> { self.0 .doc_type + .as_ref() .ok_or(anyhow::anyhow!("Missing 'type' field")) } @@ -133,10 +132,10 @@ impl Metadata { /// Converting COSE Protected Header to Metadata. pub(crate) fn from_protected_header( - protected: &coset::ProtectedHeader, report: &ProblemReport, + protected: &coset::ProtectedHeader, context: &mut DecodeContext, ) -> Self { - let metadata = InnerMetadata::from_protected_header(protected, report); - Self::from_metadata_fields(metadata, report) + let metadata = InnerMetadata::from_protected_header(protected, context); + Self::from_metadata_fields(metadata, context.report) } } @@ -144,13 +143,13 @@ impl InnerMetadata { /// Converting COSE Protected Header to Metadata fields, collecting decoding report /// issues. pub(crate) fn from_protected_header( - protected: &coset::ProtectedHeader, report: &ProblemReport, + protected: &coset::ProtectedHeader, context: &mut DecodeContext, ) -> Self { /// Context for problem report messages during decoding from COSE protected /// header. const COSE_DECODING_CONTEXT: &str = "COSE Protected Header to Metadata"; - let extra = ExtraFields::from_protected_header(protected, report); + let extra = ExtraFields::from_protected_header(protected, context.report); let mut metadata = Self { extra, ..Self::default() @@ -160,7 +159,7 @@ impl InnerMetadata { match ContentType::try_from(value) { Ok(ct) => metadata.content_type = Some(ct), Err(e) => { - report.conversion_error( + context.report.conversion_error( "COSE protected header content type", &format!("{value:?}"), &format!("Expected ContentType: {e}"), @@ -177,7 +176,7 @@ impl InnerMetadata { match ContentEncoding::try_from(value) { Ok(ce) => metadata.content_encoding = Some(ce), Err(e) => { - report.conversion_error( + context.report.conversion_error( "COSE protected header content encoding", &format!("{value:?}"), &format!("Expected ContentEncoding: {e}"), @@ -187,19 +186,23 @@ impl InnerMetadata { } } - metadata.doc_type = decode_document_field_from_protected_header::( + metadata.doc_type = cose_protected_header_find( protected, - TYPE_KEY, - COSE_DECODING_CONTEXT, - report, + |key| matches!(key, coset::Label::Text(label) if label.eq_ignore_ascii_case(TYPE_KEY)), ) - .map(|v| v.0); + .and_then(|value| { + DocType::decode( + &mut Decoder::new(&value.clone().to_vec().unwrap_or_default()), + context, + ) + .ok() + }); metadata.id = decode_document_field_from_protected_header::( protected, ID_KEY, COSE_DECODING_CONTEXT, - report, + context.report, ) .map(|v| v.0); @@ -207,7 +210,7 @@ impl InnerMetadata { protected, VER_KEY, COSE_DECODING_CONTEXT, - report, + context.report, ) .map(|v| v.0); @@ -243,10 +246,7 @@ impl TryFrom<&Metadata> for coset::Header { } builder = builder - .text_value( - TYPE_KEY.to_string(), - Value::try_from(CborUuidV4(meta.doc_type()?))?, - ) + .text_value(TYPE_KEY.to_string(), meta.doc_type()?.to_value()) .text_value( ID_KEY.to_string(), Value::try_from(CborUuidV7(meta.doc_id()?))?, diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 0c755bdcb5a..da1b4604d2a 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -5,7 +5,6 @@ pub(crate) mod utils; use std::{ collections::HashMap, - fmt, sync::LazyLock, time::{Duration, SystemTime}, }; @@ -14,7 +13,6 @@ use anyhow::Context; use catalyst_types::{ catalyst_id::{role_index::RoleId, CatalystId}, problem_report::ProblemReport, - uuid::{Uuid, UuidV4}, }; use coset::{CoseSign, CoseSignature}; use rules::{ @@ -24,28 +22,35 @@ use rules::{ use crate::{ doc_types::{ - CATEGORY_DOCUMENT_UUID_TYPE, COMMENT_DOCUMENT_UUID_TYPE, COMMENT_TEMPLATE_UUID_TYPE, - PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, PROPOSAL_DOCUMENT_UUID_TYPE, - PROPOSAL_TEMPLATE_UUID_TYPE, + deprecated::{ + CATEGORY_DOCUMENT_UUID_TYPE, COMMENT_TEMPLATE_UUID_TYPE, PROPOSAL_TEMPLATE_UUID_TYPE, + }, + COMMENT_UUID_TYPE, PROPOSAL_ACTION_DOC, PROPOSAL_COMMENT_DOC, PROPOSAL_DOC_TYPE, + PROPOSAL_UUID_TYPE, }, + metadata::DocType, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, CatalystSignedDocument, ContentEncoding, ContentType, }; /// A table representing a full set or validation rules per document id. -static DOCUMENT_RULES: LazyLock> = LazyLock::new(document_rules_init); +static DOCUMENT_RULES: LazyLock> = LazyLock::new(document_rules_init); -/// Returns an [`UuidV4`] from the provided argument, panicking if the argument is -/// invalid. +/// Returns an `DocType` from the provided argument. +/// Reduce redundant conversion. +/// This function should be used for hardcoded values, panic if conversion fail. #[allow(clippy::expect_used)] -fn expect_uuidv4(t: T) -> UuidV4 -where T: TryInto { - t.try_into().expect("Must be a valid UUID V4") +pub(crate) fn expect_doc_type(t: T) -> DocType +where + T: TryInto, + T::Error: std::fmt::Debug, +{ + t.try_into().expect("Failed to convert to DocType") } /// `DOCUMENT_RULES` initialization function #[allow(clippy::expect_used)] -fn document_rules_init() -> HashMap { +fn document_rules_init() -> HashMap { let mut document_rules_map = HashMap::new(); let proposal_document_rules = Rules { @@ -57,10 +62,10 @@ fn document_rules_init() -> HashMap { optional: false, }, content: ContentRule::Templated { - exp_template_type: expect_uuidv4(PROPOSAL_TEMPLATE_UUID_TYPE), + exp_template_type: expect_doc_type(PROPOSAL_TEMPLATE_UUID_TYPE), }, parameters: ParametersRule::Specified { - exp_parameters_type: expect_uuidv4(CATEGORY_DOCUMENT_UUID_TYPE), + exp_parameters_type: expect_doc_type(CATEGORY_DOCUMENT_UUID_TYPE), optional: true, }, doc_ref: RefRule::NotSpecified, @@ -71,7 +76,7 @@ fn document_rules_init() -> HashMap { }, }; - document_rules_map.insert(PROPOSAL_DOCUMENT_UUID_TYPE, proposal_document_rules); + document_rules_map.insert(PROPOSAL_DOC_TYPE.clone(), proposal_document_rules); let comment_document_rules = Rules { content_type: ContentTypeRule { @@ -82,14 +87,14 @@ fn document_rules_init() -> HashMap { optional: false, }, content: ContentRule::Templated { - exp_template_type: expect_uuidv4(COMMENT_TEMPLATE_UUID_TYPE), + exp_template_type: expect_doc_type(COMMENT_TEMPLATE_UUID_TYPE), }, doc_ref: RefRule::Specified { - exp_ref_type: expect_uuidv4(PROPOSAL_DOCUMENT_UUID_TYPE), + exp_ref_type: expect_doc_type(PROPOSAL_UUID_TYPE), optional: false, }, reply: ReplyRule::Specified { - exp_reply_type: expect_uuidv4(COMMENT_DOCUMENT_UUID_TYPE), + exp_reply_type: expect_doc_type(COMMENT_UUID_TYPE), optional: true, }, section: SectionRule::Specified { optional: true }, @@ -98,7 +103,7 @@ fn document_rules_init() -> HashMap { exp: &[RoleId::Role0], }, }; - document_rules_map.insert(COMMENT_DOCUMENT_UUID_TYPE, comment_document_rules); + document_rules_map.insert(PROPOSAL_COMMENT_DOC.clone(), comment_document_rules); let proposal_action_json_schema = jsonschema::options() .with_draft(jsonschema::Draft::Draft7) @@ -119,11 +124,11 @@ fn document_rules_init() -> HashMap { }, content: ContentRule::Static(ContentSchema::Json(proposal_action_json_schema)), parameters: ParametersRule::Specified { - exp_parameters_type: expect_uuidv4(CATEGORY_DOCUMENT_UUID_TYPE), + exp_parameters_type: expect_doc_type(CATEGORY_DOCUMENT_UUID_TYPE), optional: true, }, doc_ref: RefRule::Specified { - exp_ref_type: expect_uuidv4(PROPOSAL_DOCUMENT_UUID_TYPE), + exp_ref_type: expect_doc_type(PROPOSAL_UUID_TYPE), optional: false, }, reply: ReplyRule::NotSpecified, @@ -134,7 +139,7 @@ fn document_rules_init() -> HashMap { }; document_rules_map.insert( - PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + PROPOSAL_ACTION_DOC.clone(), proposal_submission_action_rules, ); @@ -164,7 +169,7 @@ where Provider: CatalystSignedDocumentProvider { return Ok(false); } - let Some(rules) = DOCUMENT_RULES.get(&doc_type.uuid()) else { + let Some(rules) = DOCUMENT_RULES.get(doc_type) else { doc.report().invalid_value( "`type`", &doc.doc_type()?.to_string(), diff --git a/rust/signed_doc/src/validator/rules/doc_ref.rs b/rust/signed_doc/src/validator/rules/doc_ref.rs index 53fec6825fe..977893b0614 100644 --- a/rust/signed_doc/src/validator/rules/doc_ref.rs +++ b/rust/signed_doc/src/validator/rules/doc_ref.rs @@ -1,13 +1,10 @@ //! `ref` rule type impl. -use catalyst_types::{ - problem_report::ProblemReport, - uuid::{Uuid, UuidV4}, -}; +use catalyst_types::problem_report::ProblemReport; use crate::{ - providers::CatalystSignedDocumentProvider, validator::utils::validate_provided_doc, - CatalystSignedDocument, + metadata::DocType, providers::CatalystSignedDocumentProvider, + validator::utils::validate_provided_doc, CatalystSignedDocument, }; /// `ref` field validation rule @@ -16,7 +13,7 @@ pub(crate) enum RefRule { /// Is 'ref' specified Specified { /// expected `type` field of the referenced doc - exp_ref_type: UuidV4, + exp_ref_type: DocType, /// optional flag for the `ref` field optional: bool, }, @@ -36,7 +33,7 @@ impl RefRule { { if let Some(doc_ref) = doc.doc_meta().doc_ref() { let ref_validator = |ref_doc: CatalystSignedDocument| { - referenced_doc_check(&ref_doc, exp_ref_type.uuid(), "ref", doc.report()) + referenced_doc_check(&ref_doc, exp_ref_type, "ref", doc.report()) }; return validate_provided_doc(&doc_ref, provider, doc.report(), ref_validator) .await; @@ -63,13 +60,14 @@ impl RefRule { /// A generic implementation of the referenced document validation. pub(crate) fn referenced_doc_check( - ref_doc: &CatalystSignedDocument, exp_ref_type: Uuid, field_name: &str, report: &ProblemReport, + ref_doc: &CatalystSignedDocument, exp_ref_type: &DocType, field_name: &str, + report: &ProblemReport, ) -> bool { let Ok(ref_doc_type) = ref_doc.doc_type() else { report.missing_field("type", "Referenced document must have type field"); return false; }; - if ref_doc_type.uuid() != exp_ref_type { + if ref_doc_type != exp_ref_type { report.invalid_value( field_name, ref_doc_type.to_string().as_str(), @@ -83,7 +81,7 @@ pub(crate) fn referenced_doc_check( #[cfg(test)] mod tests { - use catalyst_types::uuid::UuidV7; + use catalyst_types::uuid::{UuidV4, UuidV7}; use super::*; use crate::{providers::tests::TestCatalystSignedDocumentProvider, Builder}; @@ -137,7 +135,7 @@ mod tests { // all correct let rule = RefRule::Specified { - exp_ref_type, + exp_ref_type: exp_ref_type.into(), optional: false, }; let doc = Builder::new() @@ -150,7 +148,7 @@ mod tests { // all correct, `ref` field is missing, but its optional let rule = RefRule::Specified { - exp_ref_type, + exp_ref_type: exp_ref_type.into(), optional: true, }; let doc = Builder::new().build(); @@ -158,7 +156,7 @@ mod tests { // missing `ref` field, but its required let rule = RefRule::Specified { - exp_ref_type, + exp_ref_type: exp_ref_type.into(), optional: false, }; let doc = Builder::new().build(); diff --git a/rust/signed_doc/src/validator/rules/parameters.rs b/rust/signed_doc/src/validator/rules/parameters.rs index 290d158439d..41ad404df07 100644 --- a/rust/signed_doc/src/validator/rules/parameters.rs +++ b/rust/signed_doc/src/validator/rules/parameters.rs @@ -1,11 +1,9 @@ //! `parameters` rule type impl. -use catalyst_types::uuid::UuidV4; - use super::doc_ref::referenced_doc_check; use crate::{ - providers::CatalystSignedDocumentProvider, validator::utils::validate_provided_doc, - CatalystSignedDocument, + metadata::DocType, providers::CatalystSignedDocumentProvider, + validator::utils::validate_provided_doc, CatalystSignedDocument, }; /// `parameters` field validation rule @@ -14,7 +12,7 @@ pub(crate) enum ParametersRule { /// Is `parameters` specified Specified { /// expected `type` field of the parameter doc - exp_parameters_type: UuidV4, + exp_parameters_type: DocType, /// optional flag for the `parameters` field optional: bool, }, @@ -37,7 +35,7 @@ impl ParametersRule { let parameters_validator = |replied_doc: CatalystSignedDocument| { referenced_doc_check( &replied_doc, - exp_parameters_type.uuid(), + exp_parameters_type, "parameters", doc.report(), ) @@ -126,7 +124,7 @@ mod tests { // all correct let rule = ParametersRule::Specified { - exp_parameters_type, + exp_parameters_type: exp_parameters_type.into(), optional: false, }; let doc = Builder::new() @@ -139,7 +137,7 @@ mod tests { // all correct, `parameters` field is missing, but its optional let rule = ParametersRule::Specified { - exp_parameters_type, + exp_parameters_type: exp_parameters_type.into(), optional: true, }; let doc = Builder::new().build(); @@ -147,7 +145,7 @@ mod tests { // missing `parameters` field, but its required let rule = ParametersRule::Specified { - exp_parameters_type, + exp_parameters_type: exp_parameters_type.into(), optional: false, }; let doc = Builder::new().build(); diff --git a/rust/signed_doc/src/validator/rules/reply.rs b/rust/signed_doc/src/validator/rules/reply.rs index 5ac256667db..09b48ea28d9 100644 --- a/rust/signed_doc/src/validator/rules/reply.rs +++ b/rust/signed_doc/src/validator/rules/reply.rs @@ -1,11 +1,9 @@ //! `reply` rule type impl. -use catalyst_types::uuid::UuidV4; - use super::doc_ref::referenced_doc_check; use crate::{ - providers::CatalystSignedDocumentProvider, validator::utils::validate_provided_doc, - CatalystSignedDocument, + metadata::DocType, providers::CatalystSignedDocumentProvider, + validator::utils::validate_provided_doc, CatalystSignedDocument, }; /// `reply` field validation rule @@ -14,7 +12,7 @@ pub(crate) enum ReplyRule { /// Is 'reply' specified Specified { /// expected `type` field of the replied doc - exp_reply_type: UuidV4, + exp_reply_type: DocType, /// optional flag for the `ref` field optional: bool, }, @@ -35,12 +33,7 @@ impl ReplyRule { { if let Some(reply) = doc.doc_meta().reply() { let reply_validator = |replied_doc: CatalystSignedDocument| { - if !referenced_doc_check( - &replied_doc, - exp_reply_type.uuid(), - "reply", - doc.report(), - ) { + if !referenced_doc_check(&replied_doc, exp_reply_type, "reply", doc.report()) { return false; } let Some(doc_ref) = doc.doc_meta().doc_ref() else { @@ -165,7 +158,7 @@ mod tests { // all correct let rule = ReplyRule::Specified { - exp_reply_type, + exp_reply_type: exp_reply_type.into(), optional: false, }; let doc = Builder::new() @@ -179,7 +172,7 @@ mod tests { // all correct, `reply` field is missing, but its optional let rule = ReplyRule::Specified { - exp_reply_type, + exp_reply_type: exp_reply_type.into(), optional: true, }; let doc = Builder::new().build(); @@ -187,7 +180,7 @@ mod tests { // missing `reply` field, but its required let rule = ReplyRule::Specified { - exp_reply_type, + exp_reply_type: exp_reply_type.into(), optional: false, }; let doc = Builder::new() diff --git a/rust/signed_doc/src/validator/rules/template.rs b/rust/signed_doc/src/validator/rules/template.rs index 0d5b0c9aaab..13fea3b5449 100644 --- a/rust/signed_doc/src/validator/rules/template.rs +++ b/rust/signed_doc/src/validator/rules/template.rs @@ -2,12 +2,12 @@ use std::fmt::Write; -use catalyst_types::uuid::UuidV4; - use super::doc_ref::referenced_doc_check; use crate::{ - metadata::ContentType, providers::CatalystSignedDocumentProvider, - validator::utils::validate_provided_doc, CatalystSignedDocument, + metadata::{ContentType, DocType}, + providers::CatalystSignedDocumentProvider, + validator::utils::validate_provided_doc, + CatalystSignedDocument, }; /// Enum represents different content schemas, against which documents content would be @@ -23,7 +23,7 @@ pub(crate) enum ContentRule { /// Based on the 'template' field and loaded corresponding template document Templated { /// expected `type` field of the template - exp_template_type: UuidV4, + exp_template_type: DocType, }, /// Statically defined document's content schema. /// `template` field should not been specified @@ -48,12 +48,8 @@ impl ContentRule { }; let template_validator = |template_doc: CatalystSignedDocument| { - if !referenced_doc_check( - &template_doc, - exp_template_type.uuid(), - "template", - doc.report(), - ) { + if !referenced_doc_check(&template_doc, exp_template_type, "template", doc.report()) + { return false; } @@ -181,7 +177,7 @@ fn content_schema_check(doc: &CatalystSignedDocument, schema: &ContentSchema) -> #[cfg(test)] mod tests { - use catalyst_types::uuid::UuidV7; + use catalyst_types::uuid::{UuidV4, UuidV7}; use super::*; use crate::{providers::tests::TestCatalystSignedDocumentProvider, Builder}; @@ -281,7 +277,9 @@ mod tests { } // all correct - let rule = ContentRule::Templated { exp_template_type }; + let rule = ContentRule::Templated { + exp_template_type: exp_template_type.into(), + }; let doc = Builder::new() .with_json_metadata(serde_json::json!({ "template": {"id": valid_template_doc_id.to_string(), "ver": valid_template_doc_id.to_string() } @@ -300,7 +298,9 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // missing content - let rule = ContentRule::Templated { exp_template_type }; + let rule = ContentRule::Templated { + exp_template_type: exp_template_type.into(), + }; let doc = Builder::new() .with_json_metadata(serde_json::json!({ "template": {"id": valid_template_doc_id.to_string(), "ver": valid_template_doc_id.to_string() } @@ -310,7 +310,9 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // content not a json encoded - let rule = ContentRule::Templated { exp_template_type }; + let rule = ContentRule::Templated { + exp_template_type: exp_template_type.into(), + }; let doc = Builder::new() .with_json_metadata(serde_json::json!({ "template": {"id": valid_template_doc_id.to_string(), "ver": valid_template_doc_id.to_string() } @@ -341,7 +343,9 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // missing `content-type` field in the referenced doc - let rule = ContentRule::Templated { exp_template_type }; + let rule = ContentRule::Templated { + exp_template_type: exp_template_type.into(), + }; let doc = Builder::new() .with_json_metadata(serde_json::json!({ "template": {"id": missing_content_type_template_doc_id.to_string(), "ver": missing_content_type_template_doc_id.to_string() } diff --git a/rust/signed_doc/tests/comment.rs b/rust/signed_doc/tests/comment.rs index 1c746e589c7..5725e3080c7 100644 --- a/rust/signed_doc/tests/comment.rs +++ b/rust/signed_doc/tests/comment.rs @@ -8,16 +8,55 @@ mod common; #[tokio::test] async fn test_valid_comment_doc() { let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); + common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); let (template_doc, template_doc_id, template_doc_ver) = - common::create_dummy_doc(doc_types::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); + common::create_dummy_doc(doc_types::deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::COMMENT_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_COMMENT_DOC.clone(), + "id": uuid_v7.to_string(), + "ver": uuid_v7.to_string(), + "template": { + "id": template_doc_id, + "ver": template_doc_ver + }, + "ref": { + "id": proposal_doc_id, + "ver": proposal_doc_ver + } + }), + serde_json::to_vec(&serde_json::Value::Null).unwrap(), + RoleId::Role0, + ) + .unwrap(); + + let mut provider = TestCatalystSignedDocumentProvider::default(); + provider.add_document(template_doc).unwrap(); + provider.add_document(proposal_doc).unwrap(); + + let is_valid = validator::validate(&doc, &provider).await.unwrap(); + + assert!(is_valid); +} + +#[tokio::test] +async fn test_valid_comment_doc_old_type() { + let (proposal_doc, proposal_doc_id, proposal_doc_ver) = + common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + let (template_doc, template_doc_id, template_doc_ver) = + common::create_dummy_doc(doc_types::deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); + + let uuid_v7 = UuidV7::new(); + let (doc, ..) = common::create_dummy_signed_doc( + serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + // Using old (single uuid) + "type": doc_types::deprecated::COMMENT_DOCUMENT_UUID_TYPE, "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { @@ -48,9 +87,9 @@ async fn test_valid_comment_doc_with_reply() { let empty_json = serde_json::to_vec(&serde_json::json!({})).unwrap(); let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); + common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); let (template_doc, template_doc_id, template_doc_ver) = - common::create_dummy_doc(doc_types::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); + common::create_dummy_doc(doc_types::deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); let comment_doc_id = UuidV7::new(); let comment_doc_ver = UuidV7::new(); @@ -58,7 +97,7 @@ async fn test_valid_comment_doc_with_reply() { .with_json_metadata(serde_json::json!({ "id": comment_doc_id, "ver": comment_doc_ver, - "type": doc_types::COMMENT_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_COMMENT_DOC.clone(), "content-type": ContentType::Json.to_string(), "template": { "id": template_doc_id.to_string(), "ver": template_doc_ver.to_string() }, "ref": { @@ -75,7 +114,7 @@ async fn test_valid_comment_doc_with_reply() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::COMMENT_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_COMMENT_DOC.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { @@ -108,17 +147,16 @@ async fn test_valid_comment_doc_with_reply() { #[tokio::test] async fn test_invalid_comment_doc() { - let (proposal_doc, ..) = - common::create_dummy_doc(doc_types::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); + let (proposal_doc, ..) = common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); let (template_doc, template_doc_id, template_doc_ver) = - common::create_dummy_doc(doc_types::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); + common::create_dummy_doc(doc_types::deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::COMMENT_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_COMMENT_DOC.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { diff --git a/rust/signed_doc/tests/common/mod.rs b/rust/signed_doc/tests/common/mod.rs index d7ea84150b0..ae3d348f8a6 100644 --- a/rust/signed_doc/tests/common/mod.rs +++ b/rust/signed_doc/tests/common/mod.rs @@ -27,6 +27,29 @@ pub fn test_metadata() -> (UuidV7, UuidV4, serde_json::Value) { (uuid_v7, uuid_v4, metadata_fields) } +pub fn test_metadata_specific_type( + uuid_v4: Option, uuid_v7: Option, +) -> (UuidV7, UuidV4, serde_json::Value) { + let uuid_v7 = uuid_v7.unwrap_or_else(UuidV7::new); + let uuid_v4 = uuid_v4.unwrap_or_else(UuidV4::new); + + let metadata_fields = serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + "type": uuid_v4.to_string(), + "id": uuid_v7.to_string(), + "ver": uuid_v7.to_string(), + "ref": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, + "reply": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, + "template": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, + "section": "$".to_string(), + "collabs": vec!["Alex1".to_string(), "Alex2".to_string()], + "parameters": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, + }); + + (uuid_v7, uuid_v4, metadata_fields) +} + pub fn create_dummy_key_pair( role_index: RoleId, ) -> anyhow::Result<( diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index 18697703c29..795183e6990 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -10,7 +10,16 @@ mod common; #[test] fn catalyst_signed_doc_cbor_roundtrip_kid_as_id_test() { - let (_, _, metadata_fields) = common::test_metadata(); + catalyst_signed_doc_cbor_roundtrip_kid_as_id(common::test_metadata()); + catalyst_signed_doc_cbor_roundtrip_kid_as_id(common::test_metadata_specific_type( + Some(doc_types::PROPOSAL_UUID_TYPE.try_into().unwrap()), + None, + )); +} + +#[allow(clippy::unwrap_used)] +fn catalyst_signed_doc_cbor_roundtrip_kid_as_id(data: (UuidV7, UuidV4, serde_json::Value)) { + let (_, _, metadata_fields) = data; let (sk, _, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); // transform Catalyst ID URI form to the ID form let kid = kid.as_id(); @@ -29,9 +38,19 @@ fn catalyst_signed_doc_cbor_roundtrip_kid_as_id_test() { } #[tokio::test] -#[allow(clippy::too_many_lines)] async fn catalyst_signed_doc_parameters_aliases_test() { - let (_, _, metadata_fields) = common::test_metadata(); + catalyst_signed_doc_parameters_aliases(common::test_metadata()).await; + catalyst_signed_doc_parameters_aliases(common::test_metadata_specific_type( + Some(doc_types::PROPOSAL_UUID_TYPE.try_into().unwrap()), + None, + )) + .await; +} + +#[allow(clippy::unwrap_used)] +#[allow(clippy::too_many_lines)] +async fn catalyst_signed_doc_parameters_aliases(data: (UuidV7, UuidV4, serde_json::Value)) { + let (_, _, metadata_fields) = data; let (sk, pk, kid) = common::create_dummy_key_pair(RoleId::Role0).unwrap(); let mut provider = TestVerifyingKeyProvider::default(); provider.add_pk(kid.clone(), pk); @@ -215,7 +234,7 @@ fn signed_doc_with_all_fields_case() -> TestCase { valid_doc: true, post_checks: Some(Box::new({ move |doc| { - (doc.doc_type().unwrap() == uuid_v4) + (doc.doc_type().unwrap() == &DocType::from(uuid_v4)) && (doc.doc_id().unwrap() == uuid_v7) && (doc.doc_ver().unwrap() == uuid_v7) && (doc.doc_content_type().unwrap() == ContentType::Json) diff --git a/rust/signed_doc/tests/proposal.rs b/rust/signed_doc/tests/proposal.rs index 50ce1799e4a..7e6f4f21d7c 100644 --- a/rust/signed_doc/tests/proposal.rs +++ b/rust/signed_doc/tests/proposal.rs @@ -8,14 +8,46 @@ mod common; #[tokio::test] async fn test_valid_proposal_doc() { let (template_doc, template_doc_id, template_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_TEMPLATE_UUID_TYPE).unwrap(); + common::create_dummy_doc(doc_types::deprecated::PROPOSAL_TEMPLATE_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_DOC_TYPE.clone(), + "id": uuid_v7.to_string(), + "ver": uuid_v7.to_string(), + "template": { + "id": template_doc_id, + "ver": template_doc_ver + }, + }), + serde_json::to_vec(&serde_json::Value::Null).unwrap(), + RoleId::Proposer, + ) + .unwrap(); + + let mut provider = TestCatalystSignedDocumentProvider::default(); + provider.add_document(template_doc).unwrap(); + + let is_valid = validator::validate(&doc, &provider).await.unwrap(); + + assert!(is_valid); +} + +#[tokio::test] +async fn test_valid_proposal_doc_old_type() { + let (template_doc, template_doc_id, template_doc_ver) = + common::create_dummy_doc(doc_types::deprecated::PROPOSAL_TEMPLATE_UUID_TYPE).unwrap(); + + let uuid_v7 = UuidV7::new(); + let (doc, ..) = common::create_dummy_signed_doc( + serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + // Using old (single uuid) + "type": doc_types::deprecated::PROPOSAL_DOCUMENT_UUID_TYPE, "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { @@ -47,7 +79,7 @@ async fn test_valid_proposal_doc_with_empty_provider() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_DOC_TYPE.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { @@ -74,7 +106,7 @@ async fn test_invalid_proposal_doc() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_DOC_TYPE.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), // without specifying template id diff --git a/rust/signed_doc/tests/submission.rs b/rust/signed_doc/tests/submission.rs index d10c6c3952f..dc2ea5d56a4 100644 --- a/rust/signed_doc/tests/submission.rs +++ b/rust/signed_doc/tests/submission.rs @@ -8,14 +8,47 @@ mod common; #[tokio::test] async fn test_valid_submission_action() { let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); + common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_ACTION_DOC.clone(), + "id": uuid_v7.to_string(), + "ver": uuid_v7.to_string(), + "ref": { + "id": proposal_doc_id, + "ver": proposal_doc_ver + }, + }), + serde_json::to_vec(&serde_json::json!({ + "action": "final" + })) + .unwrap(), + RoleId::Proposer, + ) + .unwrap(); + + let mut provider = TestCatalystSignedDocumentProvider::default(); + provider.add_document(proposal_doc).unwrap(); + let is_valid = validator::validate(&doc, &provider).await.unwrap(); + assert!(is_valid, "{:?}", doc.problem_report()); +} + +#[tokio::test] +async fn test_valid_submission_action_old_type() { + let (proposal_doc, proposal_doc_id, proposal_doc_ver) = + common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + + let uuid_v7 = UuidV7::new(); + let (doc, ..) = common::create_dummy_signed_doc( + serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + // Using old (single uuid) + "type": doc_types::deprecated::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "ref": { @@ -47,7 +80,7 @@ async fn test_valid_submission_action_with_empty_provider() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_ACTION_DOC.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "ref": { @@ -78,7 +111,7 @@ async fn test_invalid_submission_action() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_ACTION_DOC.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), // without specifying ref @@ -98,13 +131,13 @@ async fn test_invalid_submission_action() { // corrupted JSON let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); + common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + "type": doc_types::ACTION_UUID_TYPE, "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "ref": { @@ -124,13 +157,13 @@ async fn test_invalid_submission_action() { // empty content let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); + common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + "type": doc_types::PROPOSAL_ACTION_DOC.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "ref": { From c2788a2f22e70d1bd63535b4a19f479aedb9ef1d Mon Sep 17 00:00:00 2001 From: bkioshn Date: Thu, 5 Jun 2025 11:23:14 +0700 Subject: [PATCH 06/14] fix(signed-doc): log and to value Signed-off-by: bkioshn --- rust/signed_doc/src/metadata/doc_type.rs | 42 +++++++++++++----------- rust/signed_doc/src/metadata/mod.rs | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs index a7c2ca205bb..71ff975697c 100644 --- a/rust/signed_doc/src/metadata/doc_type.rs +++ b/rust/signed_doc/src/metadata/doc_type.rs @@ -46,21 +46,6 @@ impl DocType { pub fn doc_types(&self) -> &Vec { &self.0 } - - /// Convert `DocType` to coset `Value`. - pub(crate) fn to_value(&self) -> Value { - Value::Array( - self.0 - .iter() - .map(|uuidv4| { - Value::Tag( - UUID_CBOR_TAG, - Box::new(Value::Bytes(uuidv4.uuid().as_bytes().to_vec())), - ) - }) - .collect(), - ) - } } impl Hash for DocType { @@ -275,13 +260,13 @@ impl Encode for DocType { } e.array(self.0.len().try_into().map_err(|_| { - report.other("Unable to encode array length", CONTEXT); - minicbor::encode::Error::message(format!("{CONTEXT}, unable to encode array length")) + report.invalid_encoding("Array", "Invalid array", "Valid array", CONTEXT); + minicbor::encode::Error::message(format!("{CONTEXT}, array length encoding failed")) })?)?; for id in &self.0 { id.encode(e, &mut CborContext::Tagged).map_err(|_| { - report.other("Failed to encode UUIDv4", CONTEXT); + report.invalid_encoding("UUIDv4", &id.to_string(), "Valid UUIDv4", CONTEXT); minicbor::encode::Error::message(format!("{CONTEXT}: UUIDv4 encoding failed")) })?; } @@ -317,6 +302,23 @@ impl<'de> Deserialize<'de> for DocType { } } +impl From for Value { + fn from(value: DocType) -> Self { + Value::Array( + value + .0 + .iter() + .map(|uuidv4| { + Value::Tag( + UUID_CBOR_TAG, + Box::new(Value::Bytes(uuidv4.uuid().as_bytes().to_vec())), + ) + }) + .collect(), + ) + } +} + // This is needed to preserve backward compatibility with the old solution. impl PartialEq for DocType { fn eq(&self, other: &Self) -> bool { @@ -452,9 +454,9 @@ mod tests { #[test] fn test_doc_type_to_value() { let uuid = uuid::Uuid::new_v4(); - let doc_type = DocType(vec![UuidV4::try_from(uuid).unwrap()]); + let doc_type: Value = DocType(vec![UuidV4::try_from(uuid).unwrap()]).into(); - for d in &doc_type.to_value().into_array().unwrap() { + for d in &doc_type.into_array().unwrap() { let t = d.clone().into_tag().unwrap(); assert_eq!(t.0, UUID_CBOR_TAG); assert_eq!(t.1.as_bytes().unwrap().len(), 16); diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 703b71eacc1..e36b3cf4859 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -246,7 +246,7 @@ impl TryFrom<&Metadata> for coset::Header { } builder = builder - .text_value(TYPE_KEY.to_string(), meta.doc_type()?.to_value()) + .text_value(TYPE_KEY.to_string(), meta.doc_type()?.clone().into()) .text_value( ID_KEY.to_string(), Value::try_from(CborUuidV7(meta.doc_id()?))?, From b38e384ada86f86819dcae3437004ecfdf7b50b6 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Thu, 5 Jun 2025 16:31:38 +0700 Subject: [PATCH 07/14] fix(signed-doc): add from doctype to vec uuid Signed-off-by: bkioshn --- rust/signed_doc/src/metadata/doc_type.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs index 71ff975697c..57f7c7d6ba3 100644 --- a/rust/signed_doc/src/metadata/doc_type.rs +++ b/rust/signed_doc/src/metadata/doc_type.rs @@ -91,6 +91,12 @@ impl TryFrom> for DocType { } } +impl From for Vec { + fn from(value: DocType) -> Vec { + value.0.into_iter().map(Uuid::from).collect() + } +} + impl TryFrom> for DocType { type Error = DocTypeError; @@ -276,7 +282,9 @@ impl Encode for DocType { impl<'de> Deserialize<'de> for DocType { fn deserialize(deserializer: D) -> Result - where D: Deserializer<'de> { + where + D: Deserializer<'de>, + { #[derive(Deserialize)] #[serde(untagged)] enum DocTypeInput { From 594c32ae25fb2c963deebd3cf6cf7110a189100f Mon Sep 17 00:00:00 2001 From: bkioshn Date: Fri, 6 Jun 2025 12:07:38 +0700 Subject: [PATCH 08/14] fix(signed-doc): add more doc type Signed-off-by: bkioshn --- rust/signed_doc/src/doc_types/mod.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/src/doc_types/mod.rs b/rust/signed_doc/src/doc_types/mod.rs index 2a2c22735e4..5e8924803a4 100644 --- a/rust/signed_doc/src/doc_types/mod.rs +++ b/rust/signed_doc/src/doc_types/mod.rs @@ -8,7 +8,7 @@ use deprecated::{ COMMENT_DOCUMENT_UUID_TYPE, PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, PROPOSAL_DOCUMENT_UUID_TYPE, }; -use crate::DocType; +use crate::{doc_types::deprecated::PROPOSAL_TEMPLATE_UUID_TYPE, DocType}; /// Proposal document type. #[allow(clippy::expect_used)] @@ -41,15 +41,37 @@ pub static PROPOSAL_ACTION_DOC: LazyLock = LazyLock::new(|| { .expect("Failed to convert proposal action document Uuid to DocType") }); +/// Proposal template document type. +#[allow(clippy::expect_used)] +pub static PROPOSAL_TEMPLATE: LazyLock = LazyLock::new(|| { + let ids = &[TEMPLATE_UUID_TYPE, PROPOSAL_UUID_TYPE]; + ids.to_vec() + .try_into() + .expect("Failed to convert proposal template Uuid to DocType") +}); + +/// Proposal comment template document type. +#[allow(clippy::expect_used)] +pub static PROPOSAL_COMMENT_TEMPLATE: LazyLock = LazyLock::new(|| { + let ids = &[TEMPLATE_UUID_TYPE, COMMENT_UUID_TYPE, PROPOSAL_UUID_TYPE]; + ids.to_vec() + .try_into() + .expect("Failed to convert proposal comment template Uuid to DocType") +}); + /// Submission Action UUID type. pub const SUBMISSION_ACTION_UUID_TYPE: Uuid = Uuid::from_u128(0x7892_7329_CFD9_4EA1_9C71_0E01_9B12_6A65); +/// Category UUID type. +pub const CATEGORY_UUID_TYPE: Uuid = Uuid::from_u128(0x818938C3_3139_4DAA_AFE6_974C78488E95); /// Proposal UUID type. pub const PROPOSAL_UUID_TYPE: Uuid = PROPOSAL_DOCUMENT_UUID_TYPE; /// Comment UUID type. pub const COMMENT_UUID_TYPE: Uuid = COMMENT_DOCUMENT_UUID_TYPE; /// Action UUID type. pub const ACTION_UUID_TYPE: Uuid = PROPOSAL_ACTION_DOCUMENT_UUID_TYPE; +/// Template UUID type. +pub const TEMPLATE_UUID_TYPE: Uuid = PROPOSAL_TEMPLATE_UUID_TYPE; /// Document type which will be deprecated. pub mod deprecated { From 4487babf376e343e03e1fd5100cc4343faf1e6a3 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Fri, 6 Jun 2025 12:07:57 +0700 Subject: [PATCH 09/14] fix(signed-doc): from doctype for vec string Signed-off-by: bkioshn --- rust/signed_doc/src/metadata/doc_type.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs index 57f7c7d6ba3..4c12f5aa6bf 100644 --- a/rust/signed_doc/src/metadata/doc_type.rs +++ b/rust/signed_doc/src/metadata/doc_type.rs @@ -91,9 +91,9 @@ impl TryFrom> for DocType { } } -impl From for Vec { - fn from(value: DocType) -> Vec { - value.0.into_iter().map(Uuid::from).collect() +impl From for Vec { + fn from(val: DocType) -> Self { + val.0.into_iter().map(|uuid| uuid.to_string()).collect() } } @@ -282,9 +282,7 @@ impl Encode for DocType { impl<'de> Deserialize<'de> for DocType { fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { + where D: Deserializer<'de> { #[derive(Deserialize)] #[serde(untagged)] enum DocTypeInput { From 949e4dc583563a566d15579a03b40cd823a0eed1 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Mon, 9 Jun 2025 13:16:14 +0700 Subject: [PATCH 10/14] fix(signed-doc): revert Signed-off-by: bkioshn --- rust/signed_doc/src/metadata/doc_type.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs index 4c12f5aa6bf..e5f79f88c35 100644 --- a/rust/signed_doc/src/metadata/doc_type.rs +++ b/rust/signed_doc/src/metadata/doc_type.rs @@ -91,9 +91,9 @@ impl TryFrom> for DocType { } } -impl From for Vec { - fn from(val: DocType) -> Self { - val.0.into_iter().map(|uuid| uuid.to_string()).collect() +impl From for Vec { + fn from(value: DocType) -> Vec { + value.0.into_iter().map(Uuid::from).collect() } } From 3fddef1769fe941ddb219d93e185d840ee31f43f Mon Sep 17 00:00:00 2001 From: bkioshn Date: Mon, 9 Jun 2025 13:20:39 +0700 Subject: [PATCH 11/14] fix(signed-doc): revert Signed-off-by: bkioshn --- rust/signed_doc/src/metadata/doc_type.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs index e5f79f88c35..a70e960a0c3 100644 --- a/rust/signed_doc/src/metadata/doc_type.rs +++ b/rust/signed_doc/src/metadata/doc_type.rs @@ -97,6 +97,12 @@ impl From for Vec { } } +impl From for Vec { + fn from(val: DocType) -> Self { + val.0.into_iter().map(|uuid| uuid.to_string()).collect() + } +} + impl TryFrom> for DocType { type Error = DocTypeError; From c31e75d4034df7458ded4f50637a925a449bcea7 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Tue, 10 Jun 2025 16:16:46 +0700 Subject: [PATCH 12/14] fix(signed-doc): backward compatible rule Signed-off-by: bkioshn --- rust/signed_doc/src/validator/mod.rs | 47 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index da1b4604d2a..462cbe2fb2b 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -5,7 +5,7 @@ pub(crate) mod utils; use std::{ collections::HashMap, - sync::LazyLock, + sync::{Arc, LazyLock}, time::{Duration, SystemTime}, }; @@ -22,11 +22,8 @@ use rules::{ use crate::{ doc_types::{ - deprecated::{ - CATEGORY_DOCUMENT_UUID_TYPE, COMMENT_TEMPLATE_UUID_TYPE, PROPOSAL_TEMPLATE_UUID_TYPE, - }, - COMMENT_UUID_TYPE, PROPOSAL_ACTION_DOC, PROPOSAL_COMMENT_DOC, PROPOSAL_DOC_TYPE, - PROPOSAL_UUID_TYPE, + deprecated::{self}, + PROPOSAL_ACTION_DOC, PROPOSAL_COMMENT_DOC, PROPOSAL_DOC_TYPE, }, metadata::DocType, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, @@ -34,7 +31,7 @@ use crate::{ }; /// A table representing a full set or validation rules per document id. -static DOCUMENT_RULES: LazyLock> = LazyLock::new(document_rules_init); +static DOCUMENT_RULES: LazyLock>> = LazyLock::new(document_rules_init); /// Returns an `DocType` from the provided argument. /// Reduce redundant conversion. @@ -50,7 +47,7 @@ where /// `DOCUMENT_RULES` initialization function #[allow(clippy::expect_used)] -fn document_rules_init() -> HashMap { +fn document_rules_init() -> HashMap> { let mut document_rules_map = HashMap::new(); let proposal_document_rules = Rules { @@ -62,10 +59,10 @@ fn document_rules_init() -> HashMap { optional: false, }, content: ContentRule::Templated { - exp_template_type: expect_doc_type(PROPOSAL_TEMPLATE_UUID_TYPE), + exp_template_type: expect_doc_type(deprecated::PROPOSAL_TEMPLATE_UUID_TYPE), }, parameters: ParametersRule::Specified { - exp_parameters_type: expect_doc_type(CATEGORY_DOCUMENT_UUID_TYPE), + exp_parameters_type: expect_doc_type(deprecated::CATEGORY_DOCUMENT_UUID_TYPE), optional: true, }, doc_ref: RefRule::NotSpecified, @@ -76,8 +73,6 @@ fn document_rules_init() -> HashMap { }, }; - document_rules_map.insert(PROPOSAL_DOC_TYPE.clone(), proposal_document_rules); - let comment_document_rules = Rules { content_type: ContentTypeRule { exp: ContentType::Json, @@ -87,14 +82,14 @@ fn document_rules_init() -> HashMap { optional: false, }, content: ContentRule::Templated { - exp_template_type: expect_doc_type(COMMENT_TEMPLATE_UUID_TYPE), + exp_template_type: expect_doc_type(deprecated::COMMENT_TEMPLATE_UUID_TYPE), }, doc_ref: RefRule::Specified { - exp_ref_type: expect_doc_type(PROPOSAL_UUID_TYPE), + exp_ref_type: expect_doc_type(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE), optional: false, }, reply: ReplyRule::Specified { - exp_reply_type: expect_doc_type(COMMENT_UUID_TYPE), + exp_reply_type: expect_doc_type(deprecated::COMMENT_DOCUMENT_UUID_TYPE), optional: true, }, section: SectionRule::Specified { optional: true }, @@ -103,7 +98,6 @@ fn document_rules_init() -> HashMap { exp: &[RoleId::Role0], }, }; - document_rules_map.insert(PROPOSAL_COMMENT_DOC.clone(), comment_document_rules); let proposal_action_json_schema = jsonschema::options() .with_draft(jsonschema::Draft::Draft7) @@ -124,11 +118,11 @@ fn document_rules_init() -> HashMap { }, content: ContentRule::Static(ContentSchema::Json(proposal_action_json_schema)), parameters: ParametersRule::Specified { - exp_parameters_type: expect_doc_type(CATEGORY_DOCUMENT_UUID_TYPE), + exp_parameters_type: expect_doc_type(deprecated::CATEGORY_DOCUMENT_UUID_TYPE), optional: true, }, doc_ref: RefRule::Specified { - exp_ref_type: expect_doc_type(PROPOSAL_UUID_TYPE), + exp_ref_type: expect_doc_type(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE), optional: false, }, reply: ReplyRule::NotSpecified, @@ -138,9 +132,22 @@ fn document_rules_init() -> HashMap { }, }; + let proposal_rules = Arc::new(proposal_document_rules); + let comment_rules = Arc::new(comment_document_rules); + let action_rules = Arc::new(proposal_submission_action_rules); + + document_rules_map.insert(PROPOSAL_DOC_TYPE.clone(), Arc::clone(&proposal_rules)); + document_rules_map.insert(PROPOSAL_COMMENT_DOC.clone(), Arc::clone(&comment_rules)); + document_rules_map.insert(PROPOSAL_ACTION_DOC.clone(), Arc::clone(&action_rules)); + + // Insert old rules (for backward compatibility) + document_rules_map.insert( + expect_doc_type(deprecated::COMMENT_DOCUMENT_UUID_TYPE), + Arc::clone(&comment_rules), + ); document_rules_map.insert( - PROPOSAL_ACTION_DOC.clone(), - proposal_submission_action_rules, + expect_doc_type(deprecated::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE), + Arc::clone(&action_rules), ); document_rules_map From 39fc16512483a0c5bfd4edfca24fd57721fb0b37 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Wed, 11 Jun 2025 10:40:53 +0700 Subject: [PATCH 13/14] fix(signed-doc): remove unused doc type Signed-off-by: bkioshn --- rust/signed_doc/src/doc_types/mod.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/rust/signed_doc/src/doc_types/mod.rs b/rust/signed_doc/src/doc_types/mod.rs index 5e8924803a4..08b3c703a96 100644 --- a/rust/signed_doc/src/doc_types/mod.rs +++ b/rust/signed_doc/src/doc_types/mod.rs @@ -41,24 +41,6 @@ pub static PROPOSAL_ACTION_DOC: LazyLock = LazyLock::new(|| { .expect("Failed to convert proposal action document Uuid to DocType") }); -/// Proposal template document type. -#[allow(clippy::expect_used)] -pub static PROPOSAL_TEMPLATE: LazyLock = LazyLock::new(|| { - let ids = &[TEMPLATE_UUID_TYPE, PROPOSAL_UUID_TYPE]; - ids.to_vec() - .try_into() - .expect("Failed to convert proposal template Uuid to DocType") -}); - -/// Proposal comment template document type. -#[allow(clippy::expect_used)] -pub static PROPOSAL_COMMENT_TEMPLATE: LazyLock = LazyLock::new(|| { - let ids = &[TEMPLATE_UUID_TYPE, COMMENT_UUID_TYPE, PROPOSAL_UUID_TYPE]; - ids.to_vec() - .try_into() - .expect("Failed to convert proposal comment template Uuid to DocType") -}); - /// Submission Action UUID type. pub const SUBMISSION_ACTION_UUID_TYPE: Uuid = Uuid::from_u128(0x7892_7329_CFD9_4EA1_9C71_0E01_9B12_6A65); From 54924f8a685f81cb5f49f9b08d489e5c172496b1 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Thu, 12 Jun 2025 15:04:12 +0700 Subject: [PATCH 14/14] fix(signed-doc): improve static doc types Signed-off-by: bkioshn --- rust/signed_doc/src/doc_types/mod.rs | 55 +++++++++++++----------- rust/signed_doc/src/metadata/doc_type.rs | 40 +++++++++-------- rust/signed_doc/src/validator/mod.rs | 11 +++-- rust/signed_doc/tests/comment.rs | 31 +++++++------ rust/signed_doc/tests/decoding.rs | 6 +-- rust/signed_doc/tests/proposal.rs | 6 +-- rust/signed_doc/tests/submission.rs | 24 ++++++----- 7 files changed, 95 insertions(+), 78 deletions(-) diff --git a/rust/signed_doc/src/doc_types/mod.rs b/rust/signed_doc/src/doc_types/mod.rs index 08b3c703a96..d9bf30a59cc 100644 --- a/rust/signed_doc/src/doc_types/mod.rs +++ b/rust/signed_doc/src/doc_types/mod.rs @@ -4,16 +4,13 @@ use std::sync::LazyLock; use catalyst_types::uuid::Uuid; -use deprecated::{ - COMMENT_DOCUMENT_UUID_TYPE, PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, PROPOSAL_DOCUMENT_UUID_TYPE, -}; -use crate::{doc_types::deprecated::PROPOSAL_TEMPLATE_UUID_TYPE, DocType}; +use crate::DocType; /// Proposal document type. #[allow(clippy::expect_used)] -pub static PROPOSAL_DOC_TYPE: LazyLock = LazyLock::new(|| { - let ids = &[PROPOSAL_UUID_TYPE]; +pub static PROPOSAL: LazyLock = LazyLock::new(|| { + let ids = &[PROPOSAL_BASE_TYPE]; ids.to_vec() .try_into() .expect("Failed to convert proposal document Uuid to DocType") @@ -21,8 +18,8 @@ pub static PROPOSAL_DOC_TYPE: LazyLock = LazyLock::new(|| { /// Proposal comment document type. #[allow(clippy::expect_used)] -pub static PROPOSAL_COMMENT_DOC: LazyLock = LazyLock::new(|| { - let ids = &[COMMENT_UUID_TYPE, PROPOSAL_UUID_TYPE]; +pub static PROPOSAL_COMMENT: LazyLock = LazyLock::new(|| { + let ids = &[COMMENT_BASE_TYPE, PROPOSAL_BASE_TYPE]; ids.to_vec() .try_into() .expect("Failed to convert proposal comment document Uuid to DocType") @@ -30,30 +27,40 @@ pub static PROPOSAL_COMMENT_DOC: LazyLock = LazyLock::new(|| { /// Proposal action document type. #[allow(clippy::expect_used)] -pub static PROPOSAL_ACTION_DOC: LazyLock = LazyLock::new(|| { +pub static PROPOSAL_SUBMISSION_ACTION: LazyLock = LazyLock::new(|| { let ids = &[ - ACTION_UUID_TYPE, - PROPOSAL_UUID_TYPE, - SUBMISSION_ACTION_UUID_TYPE, + ACTION_BASE_TYPE, + PROPOSAL_BASE_TYPE, + SUBMISSION_ACTION_BASE_TYPE, ]; ids.to_vec() .try_into() .expect("Failed to convert proposal action document Uuid to DocType") }); -/// Submission Action UUID type. -pub const SUBMISSION_ACTION_UUID_TYPE: Uuid = +/// -------------- Base Types -------------- +/// Action UUID base type. +pub const ACTION_BASE_TYPE: Uuid = Uuid::from_u128(0x5E60_E623_AD02_4A1B_A1AC_406D_B978_EE48); +/// Brand UUID base type. +pub const BRAND_BASE_TYPE: Uuid = Uuid::from_u128(0xEBCA_BEEB_5BC5_4F95_91E8_CAB8_CA72_4172); +/// Campaign UUID base type. +pub const CAMPAIGN_BASE_TYPE: Uuid = Uuid::from_u128(0x5EF3_2D5D_F240_462C_A7A4_BA4A_F221_FA23); +/// Category UUID base type. +pub const CATEGORY_BASE_TYPE: Uuid = Uuid::from_u128(0x8189_38C3_3139_4DAA_AFE6_974C_7848_8E95); +/// Comment UUID base type. +pub const COMMENT_BASE_TYPE: Uuid = Uuid::from_u128(0xB679_DED3_0E7C_41BA_89F8_DA62_A178_98EA); +/// Decision UUID base type. +pub const DECISION_BASE_TYPE: Uuid = Uuid::from_u128(0x788F_F4C6_D65A_451F_BB33_575F_E056_B411); +/// Moderation Action UUID base type. +pub const MODERATION_ACTION_BASE_TYPE: Uuid = + Uuid::from_u128(0xA5D2_32B8_5E03_4117_9AFD_BE32_B878_FCDD); +/// Proposal UUID base type. +pub const PROPOSAL_BASE_TYPE: Uuid = Uuid::from_u128(0x7808_D2BA_D511_40AF_84E8_C0D1_625F_DFDC); +/// Submission Action UUID base type. +pub const SUBMISSION_ACTION_BASE_TYPE: Uuid = Uuid::from_u128(0x7892_7329_CFD9_4EA1_9C71_0E01_9B12_6A65); -/// Category UUID type. -pub const CATEGORY_UUID_TYPE: Uuid = Uuid::from_u128(0x818938C3_3139_4DAA_AFE6_974C78488E95); -/// Proposal UUID type. -pub const PROPOSAL_UUID_TYPE: Uuid = PROPOSAL_DOCUMENT_UUID_TYPE; -/// Comment UUID type. -pub const COMMENT_UUID_TYPE: Uuid = COMMENT_DOCUMENT_UUID_TYPE; -/// Action UUID type. -pub const ACTION_UUID_TYPE: Uuid = PROPOSAL_ACTION_DOCUMENT_UUID_TYPE; -/// Template UUID type. -pub const TEMPLATE_UUID_TYPE: Uuid = PROPOSAL_TEMPLATE_UUID_TYPE; +/// Template UUID base type. +pub const TEMPLATE_BASE_TYPE: Uuid = Uuid::from_u128(0x0CE8_AB38_9258_4FBC_A62E_7FAA_6E58_318F); /// Document type which will be deprecated. pub mod deprecated { diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs index a70e960a0c3..85f43939131 100644 --- a/rust/signed_doc/src/metadata/doc_type.rs +++ b/rust/signed_doc/src/metadata/doc_type.rs @@ -16,10 +16,7 @@ use tracing::warn; use crate::{ decode_context::{CompatibilityPolicy, DecodeContext}, - doc_types::{ - ACTION_UUID_TYPE, COMMENT_UUID_TYPE, PROPOSAL_ACTION_DOC, PROPOSAL_COMMENT_DOC, - PROPOSAL_DOC_TYPE, PROPOSAL_UUID_TYPE, - }, + doc_types::{deprecated, PROPOSAL, PROPOSAL_COMMENT, PROPOSAL_SUBMISSION_ACTION}, }; /// List of `UUIDv4` document type. @@ -252,9 +249,11 @@ impl Decode<'_, DecodeContext<'_>> for DocType { /// fn map_doc_type(uuid: Uuid) -> anyhow::Result { match uuid { - id if id == PROPOSAL_UUID_TYPE => Ok(PROPOSAL_DOC_TYPE.clone()), - id if id == COMMENT_UUID_TYPE => Ok(PROPOSAL_COMMENT_DOC.clone()), - id if id == ACTION_UUID_TYPE => Ok(PROPOSAL_ACTION_DOC.clone()), + id if id == deprecated::PROPOSAL_DOCUMENT_UUID_TYPE => Ok(PROPOSAL.clone()), + id if id == deprecated::COMMENT_DOCUMENT_UUID_TYPE => Ok(PROPOSAL_COMMENT.clone()), + id if id == deprecated::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE => { + Ok(PROPOSAL_SUBMISSION_ACTION.clone()) + }, _ => anyhow::bail!("Unknown document type: {uuid}"), } } @@ -337,9 +336,12 @@ impl PartialEq for DocType { // List of special-case (single UUID) -> new DocType // The old one should equal to the new one let special_cases = [ - (PROPOSAL_UUID_TYPE, &*PROPOSAL_DOC_TYPE), - (COMMENT_UUID_TYPE, &*PROPOSAL_COMMENT_DOC), - (ACTION_UUID_TYPE, &*PROPOSAL_ACTION_DOC), + (deprecated::PROPOSAL_DOCUMENT_UUID_TYPE, &*PROPOSAL), + (deprecated::COMMENT_DOCUMENT_UUID_TYPE, &*PROPOSAL_COMMENT), + ( + deprecated::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + &*PROPOSAL_SUBMISSION_ACTION, + ), ]; for (uuid, expected) in special_cases { match DocType::try_from(uuid) { @@ -478,18 +480,18 @@ mod tests { #[test] fn test_doctype_equal_special_cases() { // Direct equal - let uuid = PROPOSAL_UUID_TYPE; + let uuid = deprecated::PROPOSAL_DOCUMENT_UUID_TYPE; let dt1 = DocType::try_from(vec![uuid]).unwrap(); let dt2 = DocType::try_from(vec![uuid]).unwrap(); assert_eq!(dt1, dt2); // single -> special mapped type - let single = DocType::try_from(PROPOSAL_UUID_TYPE).unwrap(); - assert_eq!(single, *PROPOSAL_DOC_TYPE); - let single = DocType::try_from(COMMENT_UUID_TYPE).unwrap(); - assert_eq!(single, *PROPOSAL_COMMENT_DOC); - let single = DocType::try_from(ACTION_UUID_TYPE).unwrap(); - assert_eq!(single, *PROPOSAL_ACTION_DOC); + let single = DocType::try_from(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); + assert_eq!(single, *PROPOSAL); + let single = DocType::try_from(deprecated::COMMENT_DOCUMENT_UUID_TYPE).unwrap(); + assert_eq!(single, *PROPOSAL_COMMENT); + let single = DocType::try_from(deprecated::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE).unwrap(); + assert_eq!(single, *PROPOSAL_SUBMISSION_ACTION); } #[test] @@ -518,10 +520,10 @@ mod tests { #[test] fn test_deserialize_special_case() { - let uuid = PROPOSAL_UUID_TYPE.to_string(); + let uuid = deprecated::PROPOSAL_DOCUMENT_UUID_TYPE.to_string(); let json = json!(uuid); let dt: DocType = serde_json::from_value(json).unwrap(); - assert_eq!(dt, *PROPOSAL_DOC_TYPE); + assert_eq!(dt, *PROPOSAL); } } diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 462cbe2fb2b..55b63d479cd 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -23,7 +23,7 @@ use rules::{ use crate::{ doc_types::{ deprecated::{self}, - PROPOSAL_ACTION_DOC, PROPOSAL_COMMENT_DOC, PROPOSAL_DOC_TYPE, + PROPOSAL, PROPOSAL_COMMENT, PROPOSAL_SUBMISSION_ACTION, }, metadata::DocType, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, @@ -136,9 +136,12 @@ fn document_rules_init() -> HashMap> { let comment_rules = Arc::new(comment_document_rules); let action_rules = Arc::new(proposal_submission_action_rules); - document_rules_map.insert(PROPOSAL_DOC_TYPE.clone(), Arc::clone(&proposal_rules)); - document_rules_map.insert(PROPOSAL_COMMENT_DOC.clone(), Arc::clone(&comment_rules)); - document_rules_map.insert(PROPOSAL_ACTION_DOC.clone(), Arc::clone(&action_rules)); + document_rules_map.insert(PROPOSAL.clone(), Arc::clone(&proposal_rules)); + document_rules_map.insert(PROPOSAL_COMMENT.clone(), Arc::clone(&comment_rules)); + document_rules_map.insert( + PROPOSAL_SUBMISSION_ACTION.clone(), + Arc::clone(&action_rules), + ); // Insert old rules (for backward compatibility) document_rules_map.insert( diff --git a/rust/signed_doc/tests/comment.rs b/rust/signed_doc/tests/comment.rs index 5725e3080c7..9c03630b79b 100644 --- a/rust/signed_doc/tests/comment.rs +++ b/rust/signed_doc/tests/comment.rs @@ -1,6 +1,8 @@ //! Integration test for comment document validation part. -use catalyst_signed_doc::{providers::tests::TestCatalystSignedDocumentProvider, *}; +use catalyst_signed_doc::{ + doc_types::deprecated, providers::tests::TestCatalystSignedDocumentProvider, *, +}; use catalyst_types::catalyst_id::role_index::RoleId; mod common; @@ -8,16 +10,16 @@ mod common; #[tokio::test] async fn test_valid_comment_doc() { let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); let (template_doc, template_doc_id, template_doc_ver) = - common::create_dummy_doc(doc_types::deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_COMMENT_DOC.clone(), + "type": doc_types::PROPOSAL_COMMENT.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { @@ -46,9 +48,9 @@ async fn test_valid_comment_doc() { #[tokio::test] async fn test_valid_comment_doc_old_type() { let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); let (template_doc, template_doc_id, template_doc_ver) = - common::create_dummy_doc(doc_types::deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( @@ -56,7 +58,7 @@ async fn test_valid_comment_doc_old_type() { "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), // Using old (single uuid) - "type": doc_types::deprecated::COMMENT_DOCUMENT_UUID_TYPE, + "type": deprecated::COMMENT_DOCUMENT_UUID_TYPE, "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { @@ -87,9 +89,9 @@ async fn test_valid_comment_doc_with_reply() { let empty_json = serde_json::to_vec(&serde_json::json!({})).unwrap(); let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); let (template_doc, template_doc_id, template_doc_ver) = - common::create_dummy_doc(doc_types::deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); let comment_doc_id = UuidV7::new(); let comment_doc_ver = UuidV7::new(); @@ -97,7 +99,7 @@ async fn test_valid_comment_doc_with_reply() { .with_json_metadata(serde_json::json!({ "id": comment_doc_id, "ver": comment_doc_ver, - "type": doc_types::PROPOSAL_COMMENT_DOC.clone(), + "type": doc_types::PROPOSAL_COMMENT.clone(), "content-type": ContentType::Json.to_string(), "template": { "id": template_doc_id.to_string(), "ver": template_doc_ver.to_string() }, "ref": { @@ -114,7 +116,7 @@ async fn test_valid_comment_doc_with_reply() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_COMMENT_DOC.clone(), + "type": doc_types::PROPOSAL_COMMENT.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { @@ -147,16 +149,17 @@ async fn test_valid_comment_doc_with_reply() { #[tokio::test] async fn test_invalid_comment_doc() { - let (proposal_doc, ..) = common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + let (proposal_doc, ..) = + common::create_dummy_doc(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); let (template_doc, template_doc_id, template_doc_ver) = - common::create_dummy_doc(doc_types::deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::COMMENT_TEMPLATE_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_COMMENT_DOC.clone(), + "type": doc_types::PROPOSAL_COMMENT.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index 795183e6990..88c15de125c 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -1,6 +1,6 @@ //! Integration test for COSE decoding part. -use catalyst_signed_doc::{providers::tests::TestVerifyingKeyProvider, *}; +use catalyst_signed_doc::{doc_types::deprecated, providers::tests::TestVerifyingKeyProvider, *}; use catalyst_types::catalyst_id::role_index::RoleId; use common::create_dummy_key_pair; use coset::TaggedCborSerializable; @@ -12,7 +12,7 @@ mod common; fn catalyst_signed_doc_cbor_roundtrip_kid_as_id_test() { catalyst_signed_doc_cbor_roundtrip_kid_as_id(common::test_metadata()); catalyst_signed_doc_cbor_roundtrip_kid_as_id(common::test_metadata_specific_type( - Some(doc_types::PROPOSAL_UUID_TYPE.try_into().unwrap()), + Some(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE.try_into().unwrap()), None, )); } @@ -41,7 +41,7 @@ fn catalyst_signed_doc_cbor_roundtrip_kid_as_id(data: (UuidV7, UuidV4, serde_jso async fn catalyst_signed_doc_parameters_aliases_test() { catalyst_signed_doc_parameters_aliases(common::test_metadata()).await; catalyst_signed_doc_parameters_aliases(common::test_metadata_specific_type( - Some(doc_types::PROPOSAL_UUID_TYPE.try_into().unwrap()), + Some(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE.try_into().unwrap()), None, )) .await; diff --git a/rust/signed_doc/tests/proposal.rs b/rust/signed_doc/tests/proposal.rs index 7e6f4f21d7c..5cc071fd012 100644 --- a/rust/signed_doc/tests/proposal.rs +++ b/rust/signed_doc/tests/proposal.rs @@ -15,7 +15,7 @@ async fn test_valid_proposal_doc() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_DOC_TYPE.clone(), + "type": doc_types::PROPOSAL.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { @@ -79,7 +79,7 @@ async fn test_valid_proposal_doc_with_empty_provider() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_DOC_TYPE.clone(), + "type": doc_types::PROPOSAL.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "template": { @@ -106,7 +106,7 @@ async fn test_invalid_proposal_doc() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_DOC_TYPE.clone(), + "type": doc_types::PROPOSAL.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), // without specifying template id diff --git a/rust/signed_doc/tests/submission.rs b/rust/signed_doc/tests/submission.rs index dc2ea5d56a4..5601f51c753 100644 --- a/rust/signed_doc/tests/submission.rs +++ b/rust/signed_doc/tests/submission.rs @@ -1,6 +1,8 @@ //! Test for proposal submission action. -use catalyst_signed_doc::{providers::tests::TestCatalystSignedDocumentProvider, *}; +use catalyst_signed_doc::{ + doc_types::deprecated, providers::tests::TestCatalystSignedDocumentProvider, *, +}; use catalyst_types::catalyst_id::role_index::RoleId; mod common; @@ -8,14 +10,14 @@ mod common; #[tokio::test] async fn test_valid_submission_action() { let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_ACTION_DOC.clone(), + "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "ref": { @@ -40,7 +42,7 @@ async fn test_valid_submission_action() { #[tokio::test] async fn test_valid_submission_action_old_type() { let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( @@ -48,7 +50,7 @@ async fn test_valid_submission_action_old_type() { "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), // Using old (single uuid) - "type": doc_types::deprecated::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, + "type": deprecated::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "ref": { @@ -80,7 +82,7 @@ async fn test_valid_submission_action_with_empty_provider() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_ACTION_DOC.clone(), + "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "ref": { @@ -111,7 +113,7 @@ async fn test_invalid_submission_action() { serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_ACTION_DOC.clone(), + "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), // without specifying ref @@ -131,13 +133,13 @@ async fn test_invalid_submission_action() { // corrupted JSON let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::ACTION_UUID_TYPE, + "type": deprecated::PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "ref": { @@ -157,13 +159,13 @@ async fn test_invalid_submission_action() { // empty content let (proposal_doc, proposal_doc_id, proposal_doc_ver) = - common::create_dummy_doc(doc_types::PROPOSAL_UUID_TYPE).unwrap(); + common::create_dummy_doc(deprecated::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap(); let uuid_v7 = UuidV7::new(); let (doc, ..) = common::create_dummy_signed_doc( serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), - "type": doc_types::PROPOSAL_ACTION_DOC.clone(), + "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), "id": uuid_v7.to_string(), "ver": uuid_v7.to_string(), "ref": {