warg_protocol/
registry.rs

1use crate::{operator::OperatorRecord, package::PackageRecord, ProtoEnvelope};
2use anyhow::bail;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6use std::time::SystemTime;
7use warg_crypto::hash::{AnyHash, Hash, HashAlgorithm, SupportedDigest};
8use warg_crypto::prefix::VisitPrefixEncode;
9use warg_crypto::{prefix, ByteVisitor, Signable, VisitBytes};
10use wasmparser::names::KebabStr;
11
12/// Type alias for registry log index
13pub type RegistryIndex = usize;
14
15/// Type alias for registry log length
16pub type RegistryLen = RegistryIndex;
17
18#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct Checkpoint {
21    pub log_root: AnyHash,
22    pub log_length: RegistryLen,
23    pub map_root: AnyHash,
24}
25
26impl prefix::VisitPrefixEncode for Checkpoint {
27    fn visit_pe<BV: ?Sized + ByteVisitor>(&self, visitor: &mut prefix::PrefixEncodeVisitor<BV>) {
28        visitor.visit_str_raw("WARG-CHECKPOINT-V0");
29        visitor.visit_unsigned(self.log_length as u64);
30        visitor.visit_str(&self.log_root.to_string());
31        visitor.visit_str(&self.map_root.to_string());
32    }
33}
34
35// Manual impls of VisitBytes for VisitPrefixEncode to avoid conflict with blanket impls
36impl VisitBytes for Checkpoint {
37    fn visit<BV: ?Sized + ByteVisitor>(&self, visitor: &mut BV) {
38        self.visit_bv(visitor);
39    }
40}
41
42#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct TimestampedCheckpoint {
45    #[serde(flatten)]
46    pub checkpoint: Checkpoint,
47    pub timestamp: u64,
48}
49
50impl TimestampedCheckpoint {
51    pub fn new(checkpoint: Checkpoint, time: SystemTime) -> anyhow::Result<Self> {
52        Ok(Self {
53            checkpoint,
54            timestamp: time.duration_since(std::time::UNIX_EPOCH)?.as_secs(),
55        })
56    }
57
58    pub fn now(checkpoint: Checkpoint) -> anyhow::Result<Self> {
59        Self::new(checkpoint, SystemTime::now())
60    }
61}
62
63impl Signable for TimestampedCheckpoint {
64    const PREFIX: &'static [u8] = b"WARG-CHECKPOINT-SIGNATURE-V0";
65}
66
67impl prefix::VisitPrefixEncode for TimestampedCheckpoint {
68    fn visit_pe<BV: ?Sized + ByteVisitor>(&self, visitor: &mut prefix::PrefixEncodeVisitor<BV>) {
69        visitor.visit_str_raw("WARG-TIMESTAMPED-CHECKPOINT-V0");
70        visitor.visit_unsigned(self.checkpoint.log_length as u64);
71        visitor.visit_str(&self.checkpoint.log_root.to_string());
72        visitor.visit_str(&self.checkpoint.map_root.to_string());
73        visitor.visit_unsigned(self.timestamp);
74    }
75}
76
77// Manual impls of VisitBytes for VisitPrefixEncode to avoid conflict with blanket impls
78impl VisitBytes for TimestampedCheckpoint {
79    fn visit<BV: ?Sized + ByteVisitor>(&self, visitor: &mut BV) {
80        self.visit_bv(visitor);
81    }
82}
83
84#[derive(Debug, Clone, Hash, PartialEq, Eq)]
85pub struct MapLeaf {
86    pub record_id: RecordId,
87}
88
89impl prefix::VisitPrefixEncode for MapLeaf {
90    fn visit_pe<BV: ?Sized + ByteVisitor>(&self, visitor: &mut prefix::PrefixEncodeVisitor<BV>) {
91        visitor.visit_str_raw("WARG-MAP-LEAF-V0");
92        visitor.visit_str(&self.record_id.0.to_string());
93    }
94}
95
96// Manual impls of VisitBytes for VisitPrefixEncode to avoid conflict with blanket impls
97impl VisitBytes for MapLeaf {
98    fn visit<BV: ?Sized + ByteVisitor>(&self, visitor: &mut BV) {
99        self.visit_bv(visitor);
100    }
101}
102
103#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct LogLeaf {
106    pub log_id: LogId,
107    pub record_id: RecordId,
108}
109
110impl prefix::VisitPrefixEncode for LogLeaf {
111    fn visit_pe<BV: ?Sized + ByteVisitor>(&self, visitor: &mut prefix::PrefixEncodeVisitor<BV>) {
112        visitor.visit_str_raw("WARG-LOG-LEAF-V0");
113        visitor.visit_str(&self.log_id.0.to_string());
114        visitor.visit_str(&self.record_id.0.to_string());
115    }
116}
117
118// Manual impls of VisitBytes for VisitPrefixEncode to avoid conflict with blanket impls
119impl VisitBytes for LogLeaf {
120    fn visit<BV: ?Sized + ByteVisitor>(&self, visitor: &mut BV) {
121        self.visit_bv(visitor);
122    }
123}
124
125/// Represents a valid package name in the registry.
126///
127/// Valid package names conform to the component model specification.
128///
129/// A valid component model package name is the format `<namespace>:<name>`,
130/// where both parts are also valid WIT identifiers (i.e. kebab-cased).
131#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
132pub struct PackageName {
133    package_name: String,
134    colon: usize,
135}
136
137impl PackageName {
138    /// Creates a package name from the given string.
139    ///
140    /// Returns an error if the given string is not a valid package name.
141    pub fn new(name: impl Into<String>) -> anyhow::Result<Self> {
142        let name = name.into();
143
144        if let Some(colon) = name.rfind(':') {
145            // Validate the namespace and name parts are valid kebab strings
146            if KebabStr::new(&name[colon + 1..]).is_some()
147                && Self::is_valid_namespace(&name[..colon])
148                && name[colon + 1..].chars().all(|c| !c.is_ascii_uppercase())
149            {
150                return Ok(Self {
151                    package_name: name,
152                    colon,
153                });
154            }
155        }
156
157        bail!("invalid package name `{name}`: expected format is `<namespace>:<name>` using lowercased characters")
158    }
159
160    /// Gets the namespace part of the package identifier.
161    pub fn namespace(&self) -> &str {
162        &self.package_name[..self.colon]
163    }
164
165    /// Gets the name part of the package identifier.
166    pub fn name(&self) -> &str {
167        &self.package_name[self.colon + 1..]
168    }
169
170    /// Check if string is a valid namespace.
171    pub fn is_valid_namespace(namespace: &str) -> bool {
172        KebabStr::new(namespace).is_some() && namespace.chars().all(|c| !c.is_ascii_uppercase())
173    }
174}
175
176impl AsRef<str> for PackageName {
177    fn as_ref(&self) -> &str {
178        &self.package_name
179    }
180}
181
182impl FromStr for PackageName {
183    type Err = anyhow::Error;
184
185    fn from_str(s: &str) -> Result<Self, Self::Err> {
186        Self::new(s)
187    }
188}
189
190impl fmt::Display for PackageName {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        write!(f, "{package_name}", package_name = self.package_name)
193    }
194}
195
196impl prefix::VisitPrefixEncode for PackageName {
197    fn visit_pe<BV: ?Sized + ByteVisitor>(&self, visitor: &mut prefix::PrefixEncodeVisitor<BV>) {
198        visitor.visit_str_raw("WARG-PACKAGE-ID-V0");
199        visitor.visit_str(&self.package_name);
200    }
201}
202
203impl VisitBytes for PackageName {
204    fn visit<BV: ?Sized + ByteVisitor>(&self, visitor: &mut BV) {
205        self.visit_bv(visitor);
206    }
207}
208
209impl Serialize for PackageName {
210    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
211        serializer.serialize_str(&self.package_name)
212    }
213}
214
215impl<'de> Deserialize<'de> for PackageName {
216    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
217        let id = String::deserialize(deserializer)?;
218        PackageName::new(id).map_err(serde::de::Error::custom)
219    }
220}
221
222#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(transparent)]
224pub struct LogId(AnyHash);
225
226impl LogId {
227    pub fn operator_log<D: SupportedDigest>() -> Self {
228        let prefix: &[u8] = b"WARG-OPERATOR-LOG-ID-V0".as_slice();
229        let hash: Hash<D> = Hash::of(prefix);
230        Self(hash.into())
231    }
232
233    pub fn package_log<D: SupportedDigest>(name: &PackageName) -> Self {
234        let prefix: &[u8] = b"WARG-PACKAGE-LOG-ID-V0:".as_slice();
235        let hash: Hash<D> = Hash::of((prefix, name));
236        Self(hash.into())
237    }
238}
239
240impl fmt::Display for LogId {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        self.0.fmt(f)
243    }
244}
245
246impl VisitBytes for LogId {
247    fn visit<BV: ?Sized + ByteVisitor>(&self, visitor: &mut BV) {
248        visitor.visit_bytes(self.0.bytes())
249    }
250}
251
252impl From<AnyHash> for LogId {
253    fn from(value: AnyHash) -> Self {
254        Self(value)
255    }
256}
257
258impl From<LogId> for AnyHash {
259    fn from(id: LogId) -> Self {
260        id.0
261    }
262}
263
264impl AsRef<[u8]> for LogId {
265    fn as_ref(&self) -> &[u8] {
266        self.0.bytes()
267    }
268}
269
270#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
271#[serde(transparent)]
272pub struct RecordId(AnyHash);
273
274impl RecordId {
275    pub fn algorithm(&self) -> HashAlgorithm {
276        self.0.algorithm()
277    }
278
279    pub fn operator_record<D: SupportedDigest>(record: &ProtoEnvelope<OperatorRecord>) -> Self {
280        let prefix: &[u8] = b"WARG-OPERATOR-LOG-RECORD-V0:".as_slice();
281        let hash: Hash<D> = Hash::of((prefix, record.content_bytes()));
282        Self(hash.into())
283    }
284
285    pub fn package_record<D: SupportedDigest>(record: &ProtoEnvelope<PackageRecord>) -> Self {
286        let prefix: &[u8] = b"WARG-PACKAGE-LOG-RECORD-V0:".as_slice();
287        let hash: Hash<D> = Hash::of((prefix, record.content_bytes()));
288        Self(hash.into())
289    }
290}
291
292impl fmt::Display for RecordId {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        self.0.fmt(f)
295    }
296}
297
298impl From<AnyHash> for RecordId {
299    fn from(value: AnyHash) -> Self {
300        Self(value)
301    }
302}
303
304impl From<RecordId> for AnyHash {
305    fn from(id: RecordId) -> Self {
306        id.0
307    }
308}
309
310impl AsRef<[u8]> for RecordId {
311    fn as_ref(&self) -> &[u8] {
312        self.0.bytes()
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use warg_crypto::hash::Sha256;
320    use warg_transparency::map::Map;
321
322    #[test]
323    fn log_id() {
324        let first = Map::<Sha256, LogId, &'static str>::default();
325        let second = first.insert(LogId::operator_log::<Sha256>(), "foobar");
326        let proof = second.prove(LogId::operator_log::<Sha256>()).unwrap();
327        assert_eq!(
328            second.root().clone(),
329            proof.evaluate(&LogId::operator_log::<Sha256>(), &"foobar")
330        );
331    }
332}