diff --git a/rust/cardano-chain-follower/examples/follow_chains.rs b/rust/cardano-chain-follower/examples/follow_chains.rs index 6d304ec6d4..41fe8094e8 100644 --- a/rust/cardano-chain-follower/examples/follow_chains.rs +++ b/rust/cardano-chain-follower/examples/follow_chains.rs @@ -343,7 +343,7 @@ async fn follow_for( || (chain_update.immutable() != last_immutable) || reached_tip || follow_all - || (updates % RUNNING_UPDATE_INTERVAL == 0) + || updates.is_multiple_of(RUNNING_UPDATE_INTERVAL) || (last_fork != chain_update.data.fork()) { current_era = this_era; diff --git a/rust/cardano-chain-follower/src/mithril_snapshot_config.rs b/rust/cardano-chain-follower/src/mithril_snapshot_config.rs index 883f3c0b3d..44e2a9b55d 100644 --- a/rust/cardano-chain-follower/src/mithril_snapshot_config.rs +++ b/rust/cardano-chain-follower/src/mithril_snapshot_config.rs @@ -495,7 +495,7 @@ fn remove_whitespace(s: &str) -> String { /// Check if a string is an even number of hex digits. fn is_hex(s: &str) -> bool { - s.chars().count() % 2 == 0 && s.chars().all(|c| c.is_ascii_hexdigit()) + s.chars().count().is_multiple_of(2) && s.chars().all(|c| c.is_ascii_hexdigit()) } #[cfg(test)] diff --git a/rust/catalyst-types/src/catalyst_id/role_index.rs b/rust/catalyst-types/src/catalyst_id/role_index.rs index 697c2a6543..b29f815342 100644 --- a/rust/catalyst-types/src/catalyst_id/role_index.rs +++ b/rust/catalyst-types/src/catalyst_id/role_index.rs @@ -28,8 +28,10 @@ pub enum RoleIdError { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, EnumIs)] #[repr(u8)] #[non_exhaustive] +#[derive(Default)] pub enum RoleId { /// Primary required role use for voting and commenting. + #[default] Role0 = 0, /// Delegated representative (dRep) that vote on behalf of delegators. DelegatedRepresentative = 1, @@ -85,12 +87,6 @@ impl RoleId { } } -impl Default for RoleId { - fn default() -> Self { - Self::Role0 - } -} - impl From for RoleId { fn from(value: u8) -> Self { match value { diff --git a/rust/hermes-ipfs/examples/pubsub.rs b/rust/hermes-ipfs/examples/pubsub.rs index 29d9e67c91..78695b677b 100644 --- a/rust/hermes-ipfs/examples/pubsub.rs +++ b/rust/hermes-ipfs/examples/pubsub.rs @@ -66,8 +66,8 @@ async fn main() -> anyhow::Result<()> { let mut event_stream = hermes_a.pubsub_events(option_topic.clone()).await?; let mut event_stream_b = hermes_b.pubsub_events(option_topic).await?; - let stream = hermes_a.pubsub_subscribe(topic.to_string()).await?; - let stream_b = hermes_b.pubsub_subscribe(topic.to_string()).await?; + let stream = hermes_a.pubsub_subscribe(topic.clone()).await?; + let stream_b = hermes_b.pubsub_subscribe(topic.clone()).await?; pin_mut!(stream); pin_mut!(stream_b); diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 38f0d35d78..5fa17f895e 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -24,7 +24,8 @@ use cbork_utils::{array::Array, decode_context::DecodeCtx, with_cbor_bytes::With pub use content::Content; use decode_context::{CompatibilityPolicy, DecodeContext}; pub use metadata::{ - ContentEncoding, ContentType, DocLocator, DocType, DocumentRef, DocumentRefs, Metadata, Section, + Chain, ContentEncoding, ContentType, DocLocator, DocType, DocumentRef, DocumentRefs, Metadata, + Section, }; use minicbor::{decode, encode, Decode, Decoder, Encode}; pub use signature::{CatalystId, Signatures}; diff --git a/rust/signed_doc/src/metadata/chain.rs b/rust/signed_doc/src/metadata/chain.rs new file mode 100644 index 0000000000..4d53514e7e --- /dev/null +++ b/rust/signed_doc/src/metadata/chain.rs @@ -0,0 +1,142 @@ +//! Document Payload Chain. +//! +//! ref: + +use std::{fmt::Display, hash::Hash}; + +use cbork_utils::{array::Array, decode_context::DecodeCtx}; + +use crate::DocumentRef; + +/// Reference to the previous Signed Document in a sequence. +#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] +pub struct Chain { + /// The consecutive sequence number of the current document + /// in the chain. + /// The very first document in a sequence is numbered `0` and it + /// *MUST ONLY* increment by one for each successive document in + /// the sequence. + /// + /// The FINAL sequence number is encoded with the current height + /// sequence value, negated. + /// + /// For example the following values for height define a chain + /// that has 5 documents in the sequence 0-4, the final height + /// is negated to indicate the end of the chain: + /// `0, 1, 2, 3, -4` + /// + /// No subsequent document can be chained to a sequence that has + /// a final chain height. + height: i32, + /// Reference to a single Signed Document. + /// + /// Can be *ONLY* omitted in the very first document in a sequence. + document_ref: Option, +} + +impl Display for Chain { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + if let Some(document_ref) = &self.document_ref { + write!(f, "height: {}, document_ref: {}", self.height, document_ref) + } else { + write!(f, "height: {}", self.height) + } + } +} + +impl minicbor::Encode<()> for Chain { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.array(if self.document_ref.is_some() { 2 } else { 1 })?; + self.height.encode(e, &mut ())?; + if let Some(doc_ref) = &self.document_ref { + doc_ref.encode(e, &mut ())?; + } + Ok(()) + } +} + +impl minicbor::Decode<'_, ()> for Chain { + fn decode( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut (), + ) -> Result { + const CONTEXT: &str = "Chain decoding"; + + let arr = Array::decode(d, &mut DecodeCtx::Deterministic)?; + + let Some(height_bytes) = arr.first() else { + return Err(minicbor::decode::Error::message(format!( + "{CONTEXT}: expected [height, ? document_ref], found empty array" + ))); + }; + + let height = minicbor::Decoder::new(height_bytes).int()?; + let height = height.try_into().map_err(minicbor::decode::Error::custom)?; + + let document_ref = arr + .get(1) + .map(|bytes| { + let mut d = minicbor::Decoder::new(bytes); + DocumentRef::decode(&mut d, &mut ()) + }) + .transpose()?; + + Ok(Self { + height, + document_ref, + }) + } +} + +#[cfg(test)] +mod tests { + use catalyst_types::uuid::UuidV7; + use minicbor::{Decode, Decoder, Encode, Encoder}; + + use super::*; + use crate::DocLocator; + + #[test] + fn test_chain_encode_decode_without_doc_ref() { + let chain = Chain { + height: 0, + document_ref: None, + }; + + let mut buf = Vec::new(); + let mut enc = Encoder::new(&mut buf); + chain.encode(&mut enc, &mut ()).unwrap(); + + let mut dec = Decoder::new(&buf); + let decoded = Chain::decode(&mut dec, &mut ()).unwrap(); + + assert_eq!(decoded, chain); + } + + #[test] + fn test_chain_encode_decode_with_doc_ref() { + let id = UuidV7::new(); + let ver = UuidV7::new(); + + let chain = Chain { + height: 3, + document_ref: Some(DocumentRef::new(id, ver, DocLocator::default())), + }; + + let mut buf = Vec::new(); + let mut enc = Encoder::new(&mut buf); + chain.encode(&mut enc, &mut ()).unwrap(); + + let mut dec = Decoder::new(&buf); + let decoded = Chain::decode(&mut dec, &mut ()).unwrap(); + + assert_eq!(decoded, chain); + } +} diff --git a/rust/signed_doc/src/metadata/document_refs/doc_locator.rs b/rust/signed_doc/src/metadata/document_refs/doc_locator.rs index 5987ddf848..8982f79f04 100644 --- a/rust/signed_doc/src/metadata/document_refs/doc_locator.rs +++ b/rust/signed_doc/src/metadata/document_refs/doc_locator.rs @@ -2,11 +2,13 @@ //! A [CBOR Encoded IPLD Content Identifier](https://github.com/ipld/cid-cbor/) //! or also known as [IPFS CID](https://docs.ipfs.tech/concepts/content-addressing/#what-is-a-cid). -use std::fmt::Display; +use std::{fmt::Display, ops::Deref, str::FromStr}; use cbork_utils::{decode_context::DecodeCtx, map::Map}; use minicbor::{Decode, Decoder, Encode}; +use crate::metadata::document_refs::DocRefError; + /// CBOR tag of IPLD content identifiers (CIDs). const CID_TAG: u64 = 42; @@ -20,23 +22,17 @@ const DOC_LOC_MAP_ITEM: u64 = 1; #[derive(Clone, Debug, Default, PartialEq, Hash, Eq)] pub struct DocLocator(Vec); -impl DocLocator { - #[must_use] - /// Length of the document locator. - pub fn len(&self) -> usize { - self.0.len() - } +impl Deref for DocLocator { + type Target = Vec; - #[must_use] - /// Is the document locator empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() + fn deref(&self) -> &Self::Target { + &self.0 } } impl From> for DocLocator { fn from(value: Vec) -> Self { - DocLocator(value) + Self(value) } } @@ -49,6 +45,38 @@ impl Display for DocLocator { } } +impl FromStr for DocLocator { + type Err = DocRefError; + + fn from_str(s: &str) -> Result { + s.strip_prefix("0x") + .map(hex::decode) + .ok_or(DocRefError::HexDecode("missing 0x prefix".to_string()))? + .map(Self) + .map_err(|e| DocRefError::HexDecode(e.to_string())) + } +} + +impl<'de> serde::Deserialize<'de> for DocLocator { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + let s = String::deserialize(deserializer)?; + s.parse::().map_err(serde::de::Error::custom) + } +} + +impl serde::Serialize for DocLocator { + fn serialize( + &self, + serializer: S, + ) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + // document_locator = { "cid" => cid } impl Decode<'_, ()> for DocLocator { fn decode( diff --git a/rust/signed_doc/src/metadata/document_refs/doc_ref.rs b/rust/signed_doc/src/metadata/document_refs/doc_ref.rs index 1fd72f6338..c775f9942b 100644 --- a/rust/signed_doc/src/metadata/document_refs/doc_ref.rs +++ b/rust/signed_doc/src/metadata/document_refs/doc_ref.rs @@ -12,13 +12,14 @@ use super::doc_locator::DocLocator; const DOC_REF_ARR_ITEM: u64 = 3; /// Reference to a Document. -#[derive(Clone, Debug, PartialEq, Hash, Eq)] +#[derive(Clone, Debug, PartialEq, Hash, Eq, serde::Serialize, serde::Deserialize)] pub struct DocumentRef { /// Reference to the Document Id id: UuidV7, /// Reference to the Document Ver ver: UuidV7, /// Document locator + #[serde(rename = "cid", default)] doc_locator: DocLocator, } diff --git a/rust/signed_doc/src/metadata/document_refs/mod.rs b/rust/signed_doc/src/metadata/document_refs/mod.rs index f13a1eac94..690fa05c27 100644 --- a/rust/signed_doc/src/metadata/document_refs/mod.rs +++ b/rust/signed_doc/src/metadata/document_refs/mod.rs @@ -190,37 +190,16 @@ impl Encode<()> for DocumentRefs { mod serde_impl { //! `serde::Deserialize` and `serde::Serialize` trait implementations - use std::str::FromStr; - - use super::{DocLocator, DocRefError, DocumentRef, DocumentRefs, UuidV7}; - - /// Old structure deserialize as map {id, ver} - #[derive(serde::Deserialize)] - struct OldRef { - /// "id": "uuidv7 - id: String, - /// "ver": "uuidv7" - ver: String, - } - - /// New structure as deserialize as map {id, ver, cid} - #[derive(serde::Deserialize, serde::Serialize)] - struct NewRef { - /// "id": "uuidv7" - id: String, - /// "ver": "uuidv7" - ver: String, - /// "cid": "0x..." - cid: String, - } + use super::{DocumentRef, DocumentRefs}; + /// A struct to support deserializing for both the old and new version of `ref`. #[derive(serde::Deserialize)] #[serde(untagged)] enum DocRefSerde { /// Old structure of document reference. - Old(OldRef), + Old(DocumentRef), /// New structure of document reference. - New(Vec), + New(Vec), } impl serde::Serialize for DocumentRefs { @@ -231,53 +210,16 @@ mod serde_impl { where S: serde::Serializer, { - let iter = self.0.iter().map(|v| { - NewRef { - id: v.id().to_string(), - ver: v.ver().to_string(), - cid: v.doc_locator().to_string(), - } - }); - serializer.collect_seq(iter) + self.0.serialize(serializer) } } impl<'de> serde::Deserialize<'de> for DocumentRefs { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de> { - let input = DocRefSerde::deserialize(deserializer)?; - match input { - DocRefSerde::Old(v) => { - let id = UuidV7::from_str(&v.id).map_err(|_| { - serde::de::Error::custom(DocRefError::StringConversion(v.id.clone())) - })?; - let ver = UuidV7::from_str(&v.ver).map_err(|_| { - serde::de::Error::custom(DocRefError::StringConversion(v.ver.clone())) - })?; - - Ok(DocumentRefs(vec![DocumentRef::new( - id, - ver, - DocLocator::default(), - )])) - }, - DocRefSerde::New(value) => { - let mut dr = vec![]; - for v in value { - let id = UuidV7::from_str(&v.id).map_err(|_| { - serde::de::Error::custom(DocRefError::StringConversion(v.id.clone())) - })?; - let ver = UuidV7::from_str(&v.ver).map_err(|_| { - serde::de::Error::custom(DocRefError::StringConversion(v.ver.clone())) - })?; - let cid = &v.cid.strip_prefix("0x").unwrap_or(&v.cid); - let locator = hex::decode(cid).map_err(|_| { - serde::de::Error::custom(DocRefError::HexDecode(v.cid.clone())) - })?; - dr.push(DocumentRef::new(id, ver, locator.into())); - } - Ok(DocumentRefs(dr)) - }, + match DocRefSerde::deserialize(deserializer)? { + DocRefSerde::Old(v) => Ok(DocumentRefs(vec![v])), + DocRefSerde::New(v) => Ok(DocumentRefs(v)), } } } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 289070a843..6be81ff836 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -4,6 +4,7 @@ use std::{ fmt::{Display, Formatter}, }; +mod chain; mod collaborators; mod content_encoding; mod content_type; @@ -13,6 +14,7 @@ mod section; mod supported_field; use catalyst_types::{catalyst_id::CatalystId, problem_report::ProblemReport, uuid::UuidV7}; +pub use chain::Chain; pub use content_encoding::ContentEncoding; pub use content_type::ContentType; pub use doc_type::DocType; @@ -132,6 +134,13 @@ impl Metadata { .and_then(SupportedField::try_as_parameters_ref) } + /// Return `chain` field. + pub fn chain(&self) -> Option<&Chain> { + self.0 + .get(&SupportedLabel::Chain) + .and_then(SupportedField::try_as_chain_ref) + } + /// Add `SupportedField` into the `Metadata`. /// /// # Warning @@ -226,6 +235,7 @@ impl Display for Metadata { writeln!(f, " section: {:?},", self.section())?; writeln!(f, " collaborators: {:?},", self.collaborators())?; writeln!(f, " parameters: {:?},", self.parameters())?; + writeln!(f, " chain: {:?},", self.chain())?; writeln!(f, " }},")?; writeln!(f, "}}") } diff --git a/rust/signed_doc/src/metadata/supported_field.rs b/rust/signed_doc/src/metadata/supported_field.rs index 08e4b938e5..191189bb77 100644 --- a/rust/signed_doc/src/metadata/supported_field.rs +++ b/rust/signed_doc/src/metadata/supported_field.rs @@ -7,8 +7,8 @@ use serde::Deserialize; use strum::{EnumDiscriminants, EnumTryAs, IntoDiscriminant as _}; use crate::{ - metadata::collaborators::Collaborators, ContentEncoding, ContentType, DocType, DocumentRefs, - Section, + metadata::collaborators::Collaborators, Chain, ContentEncoding, ContentType, DocType, + DocumentRefs, Section, }; /// COSE label. May be either a signed integer or a string. @@ -100,18 +100,20 @@ pub(crate) enum SupportedField { Ver(UuidV7) = 3, /// `type` field. Type(DocType) = 4, + /// `chain` field. + Chain(Chain) = 5, /// `reply` field. - Reply(DocumentRefs) = 5, + Reply(DocumentRefs) = 6, /// `section` field. - Section(Section) = 6, + Section(Section) = 7, /// `template` field. - Template(DocumentRefs) = 7, + Template(DocumentRefs) = 8, /// `parameters` field. - Parameters(DocumentRefs) = 8, + Parameters(DocumentRefs) = 9, /// `collaborators` field. - Collaborators(Collaborators) = 9, + Collaborators(Collaborators) = 10, /// `Content-Encoding` field. - ContentEncoding(ContentEncoding) = 10, + ContentEncoding(ContentEncoding) = 11, } impl SupportedLabel { @@ -124,6 +126,7 @@ impl SupportedLabel { Label::Str("ref") => Some(Self::Ref), Label::Str("ver") => Some(Self::Ver), Label::Str("type") => Some(Self::Type), + Label::Str("chain") => Some(Self::Chain), Label::Str("reply") => Some(Self::Reply), Label::Str("collaborators") => Some(Self::Collaborators), Label::Str("section") => Some(Self::Section), @@ -146,6 +149,7 @@ impl SupportedLabel { Self::Ref => Label::Str("ref"), Self::Ver => Label::Str("ver"), Self::Type => Label::Str("type"), + Self::Chain => Label::Str("chain"), Self::Reply => Label::Str("reply"), Self::Collaborators => Label::Str("collaborators"), Self::Section => Label::Str("section"), @@ -179,6 +183,7 @@ impl serde::ser::Serialize for SupportedField { match self { Self::Id(v) | Self::Ver(v) => v.serialize(serializer), Self::Type(v) => v.serialize(serializer), + Self::Chain(v) => v.serialize(serializer), Self::ContentType(v) => v.serialize(serializer), Self::ContentEncoding(v) => v.serialize(serializer), Self::Ref(v) | Self::Reply(v) | Self::Template(v) | Self::Parameters(v) => { @@ -205,6 +210,7 @@ impl<'de> serde::de::DeserializeSeed<'de> for SupportedLabel { SupportedLabel::Ref => Deserialize::deserialize(d).map(SupportedField::Ref), SupportedLabel::Ver => Deserialize::deserialize(d).map(SupportedField::Ver), SupportedLabel::Type => Deserialize::deserialize(d).map(SupportedField::Type), + SupportedLabel::Chain => Deserialize::deserialize(d).map(SupportedField::Chain), SupportedLabel::Reply => Deserialize::deserialize(d).map(SupportedField::Reply), SupportedLabel::Collaborators => { Deserialize::deserialize(d).map(SupportedField::Collaborators) @@ -253,6 +259,7 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext> for Option d.decode().map(SupportedField::Type), + SupportedLabel::Chain => d.decode().map(SupportedField::Chain), SupportedLabel::Reply => { d.decode_with(&mut ctx.policy().clone()) .map(SupportedField::Reply) @@ -314,6 +321,7 @@ impl minicbor::Encode<()> for SupportedField { | SupportedField::Template(document_ref) | SupportedField::Parameters(document_ref) => document_ref.encode(e, ctx), SupportedField::Type(doc_type) => doc_type.encode(e, ctx), + SupportedField::Chain(chain) => chain.encode(e, ctx), SupportedField::Collaborators(collaborators) => collaborators.encode(e, ctx), SupportedField::Section(section) => section.encode(e, ctx), SupportedField::ContentEncoding(content_encoding) => content_encoding.encode(e, ctx), diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index 633f4a7472..ad2d40ac22 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -636,6 +636,10 @@ fn signed_doc_with_complete_metadata_fields_case() -> TestCase { p_headers.bytes(b"id.catalyst://preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3")?; /* cspell:enable */ p_headers.str("parameters")?.encode_with(uuid_v7, &mut catalyst_types::uuid::CborContext::Tagged)?; + p_headers.str("chain")?; + p_headers.array(2)?; + p_headers.int(0.into())?; + p_headers.encode_with(doc_ref.clone(), &mut ())?; e.bytes(p_headers.into_writer().as_slice())?; // empty unprotected headers @@ -1367,6 +1371,7 @@ fn catalyst_signed_doc_decoding_test() { signed_doc_with_random_header_field_case("section"), signed_doc_with_random_header_field_case("collaborators"), signed_doc_with_random_header_field_case("parameters"), + signed_doc_with_random_header_field_case("chain"), signed_doc_with_random_header_field_case("content-encoding"), signed_doc_with_parameters_and_aliases_case(&["parameters", "category_id"]), signed_doc_with_parameters_and_aliases_case(&["parameters", "brand_id"]),