diff --git a/rust/signed_doc/src/doc_types/mod.rs b/rust/signed_doc/src/doc_types/mod.rs new file mode 100644 index 0000000000..e07dbb00b2 --- /dev/null +++ b/rust/signed_doc/src/doc_types/mod.rs @@ -0,0 +1,116 @@ +//! An implementation of different defined document types +//! + +mod proposal_document; + +use catalyst_types::uuid::UuidV4; +pub use proposal_document::{ProposalDocument, PROPOSAL_DOCUMENT_UUID_TYPE}; + +/// Represents different types of documents. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DocumentType { + /// A proposal document containing proposal details. + ProposalDocument, + /// A template for proposal documents, defining the expected structure. + ProposalTemplate, + /// A document representing a comment on a proposal. + CommentDocument, + /// A template for comment documents, defining the expected structure. + CommentTemplate, + /// A review document containing feedback on a proposal. + ReviewDocument, + /// A template for review documents, defining the expected structure. + ReviewTemplate, + /// A document defining parameters for a specific category. + CategoryParametersDocument, + /// A template for category parameter documents, defining the expected structure. + CategoryParametersTemplate, + /// A document containing parameters for a specific campaign. + CampaignParametersDocument, + /// A template for campaign parameter documents, defining the expected structure. + CampaignParametersTemplate, + /// A document containing brand-related parameters. + BrandParametersDocument, + /// A template for brand parameter documents, defining the expected structure. + BrandParametersTemplate, + /// A document representing an action related to a proposal. + ProposalActionDocument, + /// A public voting transaction version 2. + PublicVoteTxV2, + /// A private voting transaction version 2. + PrivateVoteTxV2, + /// A block in the immutable ledger. + ImmutableLedgerBlock, +} + +/// Proposal template `UuidV4` type. +const PROPOSAL_TEMPLATE_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x0CE8_AB38_9258_4FBC_A62E_7FAA_6E58_318F); +/// Comment document `UuidV4` type. +const COMMENT_DOCUMENT_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0xB679_DED3_0E7C_41BA_89F8_DA62_A178_98EA); +/// Comment template `UuidV4` type. +const COMMENT_TEMPLATE_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x0B84_24D4_EBFD_46E3_9577_1775_A69D_290C); +/// Review document `UuidV4` type. +const REVIEW_DOCUMENT_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0xE4CA_F5F0_098B_45FD_94F3_0702_A457_3DB5); +/// Review template `UuidV4` type. +const REVIEW_TEMPLATE_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0xEBE5_D0BF_5D86_4577_AF4D_008F_DDBE_2EDC); +/// Category parameters document `UuidV4` type. +const CATEGORY_PARAMETERS_DOCUMENT_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x48C2_0109_362A_4D32_9BBA_E0A9_CF8B_45BE); +/// Category parameters template `UuidV4` type. +const CATEGORY_PARAMETERS_TEMPLATE_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x65B1_E8B0_51F1_46A5_9970_72CD_F268_84BE); +/// Campaign parameters document `UuidV4` type. +const CAMPAIGN_PARAMETERS_DOCUMENT_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x0110_EA96_A555_47CE_8408_36EF_E6ED_6F7C); +/// Campaign parameters template `UuidV4` type. +const CAMPAIGN_PARAMETERS_TEMPLATE_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x7E8F_5FA2_44CE_49C8_BFD5_02AF_42C1_79A3); +/// Brand parameters document `UuidV4` type. +const BRAND_PARAMETERS_DOCUMENT_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x3E48_08CC_C86E_467B_9702_D60B_AA9D_1FCA); +/// Brand parameters template `UuidV4` type. +const BRAND_PARAMETERS_TEMPLATE_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0xFD3C_1735_80B1_4EEA_8D63_5F43_6D97_EA31); +/// Proposal action document `UuidV4` type. +const PROPOSAL_ACTION_DOCUMENT_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x5E60_E623_AD02_4A1B_A1AC_406D_B978_EE48); +/// Public vote transaction v2 `UuidV4` type. +const PUBLIC_VOTE_TX_V2_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x8DE5_586C_E998_4B95_8742_7BE3_C859_2803); +/// Private vote transaction v2 `UuidV4` type. +const PRIVATE_VOTE_TX_V2_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0xE78E_E18D_F380_44C1_A852_80AA_6ECB_07FE); +/// Immutable ledger block `UuidV4` type. +const IMMUTABLE_LEDGER_BLOCK_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0xD9E7_E6CE_2401_4D7D_9492_F4F7_C642_41C3); + +impl TryFrom for DocumentType { + type Error = anyhow::Error; + + fn try_from(uuid: UuidV4) -> Result { + match uuid.uuid() { + PROPOSAL_DOCUMENT_UUID_TYPE => Ok(DocumentType::ProposalDocument), + PROPOSAL_TEMPLATE_UUID_TYPE => Ok(DocumentType::ProposalTemplate), + COMMENT_DOCUMENT_UUID_TYPE => Ok(DocumentType::CommentDocument), + COMMENT_TEMPLATE_UUID_TYPE => Ok(DocumentType::CommentTemplate), + REVIEW_DOCUMENT_UUID_TYPE => Ok(DocumentType::ReviewDocument), + REVIEW_TEMPLATE_UUID_TYPE => Ok(DocumentType::ReviewTemplate), + CATEGORY_PARAMETERS_DOCUMENT_UUID_TYPE => Ok(DocumentType::CategoryParametersDocument), + CATEGORY_PARAMETERS_TEMPLATE_UUID_TYPE => Ok(DocumentType::CategoryParametersTemplate), + CAMPAIGN_PARAMETERS_DOCUMENT_UUID_TYPE => Ok(DocumentType::CampaignParametersDocument), + CAMPAIGN_PARAMETERS_TEMPLATE_UUID_TYPE => Ok(DocumentType::CampaignParametersTemplate), + BRAND_PARAMETERS_DOCUMENT_UUID_TYPE => Ok(DocumentType::BrandParametersDocument), + BRAND_PARAMETERS_TEMPLATE_UUID_TYPE => Ok(DocumentType::BrandParametersTemplate), + PROPOSAL_ACTION_DOCUMENT_UUID_TYPE => Ok(DocumentType::ProposalActionDocument), + PUBLIC_VOTE_TX_V2_UUID_TYPE => Ok(DocumentType::PublicVoteTxV2), + PRIVATE_VOTE_TX_V2_UUID_TYPE => Ok(DocumentType::PrivateVoteTxV2), + IMMUTABLE_LEDGER_BLOCK_UUID_TYPE => Ok(DocumentType::ImmutableLedgerBlock), + _ => anyhow::bail!("Unsupported document type"), + } + } +} diff --git a/rust/signed_doc/src/doc_types/proposal_document.rs b/rust/signed_doc/src/doc_types/proposal_document.rs new file mode 100644 index 0000000000..7fffae7f07 --- /dev/null +++ b/rust/signed_doc/src/doc_types/proposal_document.rs @@ -0,0 +1,68 @@ +//! Proposal Document object implementation +//! + +use catalyst_types::problem_report::ProblemReport; + +use crate::{error::CatalystSignedDocError, CatalystSignedDocument}; + +/// Proposal document `UuidV4` type. +pub const PROPOSAL_DOCUMENT_UUID_TYPE: uuid::Uuid = + uuid::Uuid::from_u128(0x7808_D2BA_D511_40AF_84E8_C0D1_625F_DFDC); + +/// Proposal Document struct +pub struct ProposalDocument { + /// Proposal document content data + /// TODO: change it to `serde_json::Value` type + #[allow(dead_code)] + content: Vec, +} + +impl ProposalDocument { + /// Try to build `ProposalDocument` from `CatalystSignedDoc` doing all necessary + /// stateless verifications, + #[allow(dead_code)] + pub(crate) fn from_signed_doc( + doc: &CatalystSignedDocument, error_report: &ProblemReport, + ) -> anyhow::Result { + /// Context for error messages. + const CONTEXT: &str = "Catalyst Signed Document to Proposal Document"; + let mut failed = false; + + if doc.doc_type().uuid() != PROPOSAL_DOCUMENT_UUID_TYPE { + error_report.invalid_value( + "`type`", + &doc.doc_type().to_string(), + &format!("Proposal Document type UUID value is {PROPOSAL_DOCUMENT_UUID_TYPE}"), + CONTEXT, + ); + failed = true; + } + + // TODO add other validation + + if failed { + anyhow::bail!("Failed to build `ProposalDocument` from `CatalystSignedDoc`"); + } + + let content = doc.doc_content().decoded_bytes().to_vec(); + Ok(Self { content }) + } + + /// A comprehensive validation of the `ProposalDocument` content. + #[allow(clippy::unused_self)] + pub(crate) fn validate_with_report(&self, _doc_getter: F, _error_report: &ProblemReport) + where F: FnMut() -> Option { + // TODO: implement the rest of the validation + } +} + +impl TryFrom for ProposalDocument { + type Error = CatalystSignedDocError; + + fn try_from(doc: CatalystSignedDocument) -> Result { + let error_report = ProblemReport::new("Proposal Document"); + let res = Self::from_signed_doc(&doc, &error_report) + .map_err(|e| CatalystSignedDocError::new(error_report, e))?; + Ok(res) + } +} diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index ff2bf290f0..7e236937d8 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -2,10 +2,12 @@ mod builder; mod content; +pub mod doc_types; pub mod error; mod metadata; mod signature; mod utils; +pub mod validator; use std::{ convert::TryFrom, @@ -15,11 +17,12 @@ use std::{ pub use builder::Builder; use catalyst_types::problem_report::ProblemReport; +pub use catalyst_types::uuid::{UuidV4, UuidV7}; pub use content::Content; use coset::{CborSerializable, Header}; use ed25519_dalek::VerifyingKey; use error::CatalystSignedDocError; -pub use metadata::{DocumentRef, ExtraFields, Metadata, UuidV4, UuidV7}; +pub use metadata::{DocumentRef, ExtraFields, Metadata}; pub use minicbor::{decode, encode, Decode, Decoder, Encode}; pub use signature::{KidUri, Signatures}; use utils::context::DecodeSignDocCtx; diff --git a/rust/signed_doc/src/metadata/document_id.rs b/rust/signed_doc/src/metadata/document_id.rs deleted file mode 100644 index 931373322d..0000000000 --- a/rust/signed_doc/src/metadata/document_id.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Document ID. -use std::fmt::{Display, Formatter}; - -use coset::cbor::Value; - -use super::{encode_cbor_uuid, UuidV7}; - -/// Catalyst Document ID. -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] -#[serde(from = "UuidV7")] -pub struct DocumentId(UuidV7); - -impl DocumentId { - /// Returns the `uuid::Uuid` type. - #[must_use] - pub fn uuid(&self) -> uuid::Uuid { - self.0.uuid() - } -} - -impl Display for DocumentId { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.0) - } -} - -impl From for DocumentId { - fn from(uuid: UuidV7) -> Self { - Self(uuid) - } -} - -impl From for UuidV7 { - fn from(value: DocumentId) -> Self { - value.0 - } -} - -impl TryFrom for Value { - type Error = anyhow::Error; - - fn try_from(value: DocumentId) -> Result { - encode_cbor_uuid(value.0) - } -} diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index 367e66a3c6..eaffaa58b2 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -13,6 +13,17 @@ pub struct DocumentRef { pub ver: Option, } +impl DocumentRef { + /// Determine if internal `UUID`s are valid. + #[must_use] + pub fn is_valid(&self) -> bool { + match self.ver { + Some(ver) => self.id.is_valid() && ver.is_valid() && ver >= self.id, + None => self.id.is_valid(), + } + } +} + impl TryFrom for Value { type Error = anyhow::Error; diff --git a/rust/signed_doc/src/metadata/document_type.rs b/rust/signed_doc/src/metadata/document_type.rs deleted file mode 100644 index 6c0b355289..0000000000 --- a/rust/signed_doc/src/metadata/document_type.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Document Type. -use std::fmt::{Display, Formatter}; - -use coset::cbor::Value; - -use super::{encode_cbor_uuid, UuidV4}; - -/// Catalyst Document Type. -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] -#[serde(from = "UuidV4")] -pub struct DocumentType(UuidV4); - -impl DocumentType { - /// Returns the `uuid::Uuid` type. - #[must_use] - pub fn uuid(&self) -> uuid::Uuid { - self.0.uuid() - } -} - -impl Display for DocumentType { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.0) - } -} - -impl From for DocumentType { - fn from(value: UuidV4) -> Self { - Self(value) - } -} - -impl From for UuidV4 { - fn from(value: DocumentType) -> Self { - value.0 - } -} - -impl TryFrom for Value { - type Error = anyhow::Error; - - fn try_from(value: DocumentType) -> Result { - encode_cbor_uuid(value.0) - } -} diff --git a/rust/signed_doc/src/metadata/document_version.rs b/rust/signed_doc/src/metadata/document_version.rs deleted file mode 100644 index 5aea2d0275..0000000000 --- a/rust/signed_doc/src/metadata/document_version.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Document Version. -use std::fmt::{Display, Formatter}; - -use coset::cbor::Value; - -use super::{encode_cbor_uuid, UuidV7}; - -/// Catalyst Document Version. -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] -pub struct DocumentVersion(UuidV7); - -impl DocumentVersion { - /// Returns the `uuid::Uuid` type. - #[must_use] - pub fn uuid(&self) -> uuid::Uuid { - self.0.uuid() - } -} - -impl Display for DocumentVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.0) - } -} - -impl From for DocumentVersion { - fn from(value: UuidV7) -> Self { - Self(value) - } -} - -impl From for UuidV7 { - fn from(value: DocumentVersion) -> Self { - value.0 - } -} - -impl TryFrom for Value { - type Error = anyhow::Error; - - fn try_from(value: DocumentVersion) -> Result { - encode_cbor_uuid(value.0) - } -} diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index cc7619b788..bbc42be07d 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -4,23 +4,19 @@ use std::fmt::{Display, Formatter}; mod algorithm; mod content_encoding; mod content_type; -mod document_id; mod document_ref; -mod document_type; -mod document_version; mod extra_fields; use algorithm::Algorithm; use anyhow::{anyhow, bail}; -use catalyst_types::problem_report::ProblemReport; -pub use catalyst_types::uuid::{CborContext, UuidV4, UuidV7}; +use catalyst_types::{ + problem_report::ProblemReport, + uuid::{CborContext, UuidV4, UuidV7}, +}; pub use content_encoding::ContentEncoding; pub use content_type::ContentType; use coset::{iana::CoapContentFormat, CborSerializable}; -pub use document_id::DocumentId; pub use document_ref::DocumentRef; -pub use document_type::DocumentType; -pub use document_version::DocumentVersion; pub use extra_fields::ExtraFields; /// `content_encoding` field COSE key value @@ -42,11 +38,11 @@ pub struct Metadata { alg: Algorithm, /// Document Type `UUIDv4`. #[serde(rename = "type")] - doc_type: DocumentType, + doc_type: UuidV4, /// Document ID `UUIDv7`. - id: DocumentId, + id: UuidV7, /// Document Version `UUIDv7`. - ver: DocumentVersion, + ver: UuidV7, /// Document Payload Content Type. #[serde(rename = "content-type")] content_type: ContentType, @@ -68,19 +64,19 @@ impl Metadata { /// Return Document Type `UUIDv4`. #[must_use] pub fn doc_type(&self) -> UuidV4 { - self.doc_type.into() + self.doc_type } /// Return Document ID `UUIDv7`. #[must_use] pub fn doc_id(&self) -> UuidV7 { - self.id.into() + self.id } /// Return Document Version `UUIDv7`. #[must_use] pub fn doc_ver(&self) -> UuidV7 { - self.ver.into() + self.ver } /// Returns the Document Content Type, if any. @@ -244,9 +240,9 @@ impl Metadata { } Ok(Self { - doc_type: doc_type.into(), - id: id.into(), - ver: ver.into(), + doc_type, + id, + ver, alg: algorithm, content_encoding, content_type, @@ -288,9 +284,9 @@ impl TryFrom<&Metadata> for coset::Header { } builder = builder - .text_value(TYPE_KEY.to_string(), meta.doc_type.try_into()?) - .text_value(ID_KEY.to_string(), meta.id.try_into()?) - .text_value(VER_KEY.to_string(), meta.ver.try_into()?); + .text_value(TYPE_KEY.to_string(), encode_cbor_uuid(meta.doc_type)?) + .text_value(ID_KEY.to_string(), encode_cbor_uuid(meta.id)?) + .text_value(VER_KEY.to_string(), encode_cbor_uuid(meta.ver)?); builder = meta.extra.fill_cose_header_fields(builder)?; @@ -326,9 +322,7 @@ pub(crate) fn encode_cbor_uuid>( /// Decode `From` type from `coset::cbor::Value`. /// /// This is used to decode `UuidV4` and `UuidV7` types. -pub(crate) fn decode_cbor_uuid< - T: for<'a> minicbor::decode::Decode<'a, CborContext> + TryFrom, ->( +pub(crate) fn decode_cbor_uuid minicbor::decode::Decode<'a, CborContext>>( value: coset::cbor::Value, ) -> anyhow::Result { match value.to_vec() { diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs new file mode 100644 index 0000000000..7009e66198 --- /dev/null +++ b/rust/signed_doc/src/validator/mod.rs @@ -0,0 +1,71 @@ +//! Catalyst Signed Documents validation + +use catalyst_types::problem_report::ProblemReport; + +use crate::{ + doc_types::{DocumentType, ProposalDocument}, + error::CatalystSignedDocError, + CatalystSignedDocument, +}; + +/// A comprehensive validation of the `CatalystSignedDocument`, +/// including a signature verification and document type based validation. +/// +/// # Errors +/// +/// Returns a report of validation failures and the source error. +pub fn validate( + doc: &CatalystSignedDocument, doc_getter: F, +) -> Result<(), CatalystSignedDocError> +where F: FnMut() -> Option { + let error_report = ProblemReport::new("Catalyst Signed Document Validation"); + + let doc_type: DocumentType = match doc.doc_type().try_into() { + Ok(doc_type) => doc_type, + Err(e) => { + error_report.invalid_value( + "`type`", + &doc.doc_type().to_string(), + &e.to_string(), + "verifying document type", + ); + return Err(CatalystSignedDocError::new( + error_report, + anyhow::anyhow!("Validation of the Catalyst Signed Document failed"), + )); + }, + }; + + #[allow(clippy::match_same_arms)] + match doc_type { + DocumentType::ProposalDocument => { + if let Ok(proposal_doc) = ProposalDocument::from_signed_doc(doc, &error_report) { + proposal_doc.validate_with_report(doc_getter, &error_report); + } + }, + DocumentType::ProposalTemplate => {}, + DocumentType::CommentDocument => {}, + DocumentType::CommentTemplate => {}, + DocumentType::ReviewDocument => {}, + DocumentType::ReviewTemplate => {}, + DocumentType::CategoryParametersDocument => {}, + DocumentType::CategoryParametersTemplate => {}, + DocumentType::CampaignParametersDocument => {}, + DocumentType::CampaignParametersTemplate => {}, + DocumentType::BrandParametersDocument => {}, + DocumentType::BrandParametersTemplate => {}, + DocumentType::ProposalActionDocument => {}, + DocumentType::PublicVoteTxV2 => {}, + DocumentType::PrivateVoteTxV2 => {}, + DocumentType::ImmutableLedgerBlock => {}, + } + + if error_report.is_problematic() { + return Err(CatalystSignedDocError::new( + error_report, + anyhow::anyhow!("Validation of the Catalyst Signed Document failed"), + )); + } + + Ok(()) +}