borderless_pkg/
lib.rs

1//! Definition of a borderless wasm package
2//!
3//! SmartContracts aswell as SoftwareAgents are compiled to webassembly, to be then executed on our runtime.
4//! However, since it is not very handy to directly work with the compiled modules, we defined a package format,
5//! that bundles the `.wasm` module together with some meta information about the package.
6//!
7use borderless_hash::Hash256;
8use git_info::GitInfo;
9use serde::{Deserialize, Serialize};
10
11pub use crate::author::Author;
12use crate::dto::*;
13pub use crate::semver::SemVer;
14
15mod author;
16pub mod dto;
17pub mod git_info;
18pub mod semver;
19
20// TODO: When using this with the CLI, it may be beneficial to add builders to all of those types.
21// However, this should be gated behind a feature flag, as other consumers of this library only require the parsing logic.
22
23/// Defines how to fetch the wasm code from a registry
24///
25/// For now the definition is quite basic, but we can later expand on this and support different
26/// types of registries, that may have different interfaces.
27///
28/// Right now the idea is to use the OCI standard here, so the full URI of some package will be
29/// `registry_hostname/namespace/pkg-name:pkg-version`
30///
31/// This then has to be translated into a proper URL based on the registry type to fetch the actual content.
32///
33/// Please note: The definition of the package-name and version is not part of the `Registry`.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Registry {
36    /// Type of registry. If none given, the OCI standard is used.
37    #[serde(default)]
38    pub registry_type: Option<String>, // NOTE: We can expand on that later
39
40    /// Base-URL of the registry
41    pub registry_hostname: String,
42
43    /// Namespace in the registry
44    ///
45    /// This can be an organization or arbitrary namespace.
46    pub namespace: String,
47}
48
49impl Registry {
50    pub fn into_dto(self) -> RegistryDto {
51        self.into()
52    }
53}
54
55/// Specifies the source type - aka how to get the wasm module
56///
57/// This is either a [`Registry`], which can be used to download the `.wasm` blob,
58/// or it is an inline definition, that just contains the compiled `.wasm` module.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(untagged)]
61pub enum SourceType {
62    /// Registry, where the wasm module can be fetched from
63    Registry { registry: Registry },
64
65    /// Compiled wasm module
66    Wasm {
67        /// Compiled wasm module
68        #[serde(with = "code_as_base64")]
69        wasm: Vec<u8>,
70
71        /// Git information
72        ///
73        /// When creating the package, this information can be added to aid with debugging.
74        /// In general, the version *should* be enough to describe the package,
75        /// but when not working with a registry (which does some sanity checks and
76        /// e.g. forbids changing the code without increasing the version number),
77        /// it is easier to make mistakes along the way. Being able to track back the origin of the
78        /// compiled module to its git hash is very helpful.
79        #[serde(default)]
80        #[serde(skip_serializing_if = "Option::is_none")]
81        git_info: Option<GitInfo>,
82    },
83}
84
85mod code_as_base64 {
86    use base64::prelude::*;
87    use serde::{Deserialize, Serialize};
88    use serde::{Deserializer, Serializer};
89
90    pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
91        let base64 = BASE64_STANDARD.encode(v);
92        String::serialize(&base64, s)
93    }
94
95    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
96        let b64 = String::deserialize(d)?;
97        BASE64_STANDARD
98            .decode(b64.as_bytes())
99            .map_err(serde::de::Error::custom)
100    }
101}
102
103/// Specifies the complete source of a wasm module
104///
105/// This contains the version, concrete source ( either local bytes or link to a remote registry ) and hash digest of the compiled module.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct Source {
108    /// Version of the wasm module
109    pub version: SemVer,
110
111    /// Sha3-256 digest of the module
112    pub digest: Hash256,
113
114    /// Concrete source - see [`SourceType`]
115    #[serde(flatten)]
116    pub code: SourceType,
117}
118
119impl Source {
120    /// 'flattens' the `Source` to create a [`SourceFlattened`]
121    ///
122    /// Useful for serializers that do not support advanced serde features.
123    pub fn flatten(self) -> SourceFlattened {
124        let (registry, wasm, git_info) = match self.code {
125            SourceType::Registry { registry } => (Some(registry), None, None),
126            SourceType::Wasm { wasm, git_info } => (None, Some(wasm), git_info),
127        };
128        SourceFlattened {
129            version: self.version,
130            digest: self.digest,
131            registry,
132            wasm,
133            git_info,
134        }
135    }
136}
137
138/// A 'flattened' version of [`Source`]
139///
140/// Some serializers do not support all serde features, like untagged enums or flattening.
141/// This struct is a replacement for [`Source`], in case your serializer cannot properly serialize the `Source` type.
142/// In this version, the content of [`SourceType`] is directly inlined into the struct definition using options.
143/// Also the base64 encoding of the wasm bytes is removed in this version.
144///
145/// You can see this as an "on-disk" version of `Source`. For transfer over the wire (especially with json !) you should use [`Source`].
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct SourceFlattened {
148    pub version: SemVer,
149
150    /// Sha3-256 digest of the module
151    pub digest: Hash256,
152
153    #[serde(default)]
154    registry: Option<Registry>,
155
156    #[serde(default)]
157    #[serde(with = "serde_bytes")]
158    wasm: Option<Vec<u8>>,
159
160    #[serde(default)]
161    git_info: Option<GitInfo>,
162}
163
164impl SourceFlattened {
165    /// 'unflattens' the data back into a [`Source`]
166    ///
167    /// Inverse operation of [`Source::flatten`].
168    ///
169    /// # Safety
170    ///
171    /// This function panics, if the `SourceFlattened` cannot be converted into a `Source`,
172    /// because both `registry` and `wasm` are set to either `None` or `Some` ( it should be either or ).
173    pub fn unflatten(self) -> Source {
174        let code = match (self.registry, self.wasm) {
175            (Some(registry), None) => SourceType::Registry { registry  },
176            (None, Some(wasm)) => SourceType::Wasm { wasm, git_info: self.git_info  },
177            _ => panic!("Failed to convert into `Source` - either `registry` or `wasm` must be set, but neither both or none"),
178        };
179        Source {
180            version: self.version,
181            digest: self.digest,
182            code,
183        }
184    }
185}
186
187/// Package metadata
188///
189/// Contains things like the authors, license, link to documentation etc.
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191pub struct PkgMeta {
192    /// Authors of the package
193    #[serde(default)]
194    pub authors: Vec<Author>,
195
196    /// A description of the package
197    #[serde(default)]
198    pub description: Option<String>,
199
200    /// URL of the package documentation
201    #[serde(default)]
202    pub documentation: Option<String>,
203
204    /// License information
205    ///
206    /// SPDX 2.3 license expression
207    #[serde(default)]
208    pub license: Option<String>,
209
210    /// URL of the package source repository
211    #[serde(default)]
212    pub repository: Option<String>,
213}
214
215impl PkgMeta {
216    pub fn into_dto(self) -> PkgMetaDto {
217        self.into()
218    }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
222#[serde(rename_all = "lowercase")]
223pub enum PkgType {
224    Contract,
225    Agent,
226}
227
228/// Capabilities of a SW-Agent
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct Capabilities {
231    /// Weather or not the agent is allowed to make http-calls
232    pub network: bool,
233    /// Weather or not the agent is allowed to establish websocket connections
234    pub websocket: bool,
235    /// URLs that the agent is allowed to call
236    pub url_whitelist: Vec<String>,
237}
238
239/// Definition of a wasm package
240///
241/// Contains the necessary information about the source, a name for the package
242/// and (optional) package metadata.
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct WasmPkg {
245    /// Name of the package
246    pub name: String,
247
248    /// Name of the application that this package is a part of
249    ///
250    /// An application is just an abstraction for multiple wasm packages.
251    /// It can be further split into application modules.
252    ///
253    /// The full specifier for the package would be (if application and app-modules are used):
254    /// `<app_name>/<app_module>/<pkg-name>`
255    #[serde(default)]
256    pub app_name: Option<String>,
257
258    /// Name of the application module that this package is a part of
259    ///
260    /// An application module is a subset of wasm modules in an application.
261    ///
262    /// The full specifier for the package would be (if application and app-modules are used):
263    /// `<app_name>/<app_module>/<pkg-name>`
264    #[serde(default)]
265    pub app_module: Option<String>,
266
267    /// (Networking) Capabilities of the package
268    ///
269    /// This is only used for software agents, which can make network calls and may use a websocket.
270    /// The capabilities are registered in the runtime, so that the agent cannot make any other network
271    /// calls than specified by the url-whitelist in [`Capabilities`].
272    #[serde(default)]
273    pub capabilities: Option<Capabilities>,
274
275    /// Package type (contract or agent)
276    pub pkg_type: PkgType,
277
278    /// Package metadata
279    #[serde(default)]
280    pub meta: PkgMeta,
281
282    /// Package source
283    pub source: Source,
284}
285
286impl WasmPkg {
287    /// Split the `Source` out of the `WasmPkg`, so we can store or handle it separately
288    pub fn into_def_and_source(self) -> (WasmPkgNoSource, Source) {
289        let pkg_def = WasmPkgNoSource {
290            name: self.name,
291            app_name: self.app_name,
292            app_module: self.app_module,
293            capabilities: self.capabilities,
294            pkg_type: self.pkg_type,
295            meta: self.meta,
296        };
297        let source = self.source;
298        (pkg_def, source)
299    }
300
301    /// Merge the `Source` back into the `WasmPkg`
302    pub fn from_def_and_source(pkg_def: WasmPkgNoSource, source: Source) -> Self {
303        Self {
304            name: pkg_def.name,
305            app_name: pkg_def.app_name,
306            app_module: pkg_def.app_module,
307            capabilities: pkg_def.capabilities,
308            pkg_type: pkg_def.pkg_type,
309            meta: pkg_def.meta,
310            source,
311        }
312    }
313
314    pub fn into_dto(self) -> WasmPkgDto {
315        self.into()
316    }
317}
318
319/// Definition of a wasm package - without the actual source
320///
321/// There are cases where you want to handle the package definition and the source seperately,
322/// so we need a type to represent a `WasmPkg` without the actual `Source`.
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct WasmPkgNoSource {
325    /// Name of the package
326    pub name: String,
327
328    /// Name of the application that this package is a part of
329    ///
330    /// An application is just an abstraction for multiple wasm packages.
331    /// It can be further split into application modules.
332    ///
333    /// The full specifier for the package would be (if application and app-modules are used):
334    /// `<app_name>/<app_module>/<pkg-name>`
335    #[serde(default)]
336    pub app_name: Option<String>,
337
338    /// Name of the application module that this package is a part of
339    ///
340    /// An application module is a subset of wasm modules in an application.
341    ///
342    /// The full specifier for the package would be (if application and app-modules are used):
343    /// `<app_name>/<app_module>/<pkg-name>`
344    #[serde(default)]
345    pub app_module: Option<String>,
346
347    /// (Networking) Capabilities of the package
348    ///
349    /// This is only used for software agents, which can make network calls and may use a websocket.
350    /// The capabilities are registered in the runtime, so that the agent cannot make any other network
351    /// calls than specified by the url-whitelist in [`Capabilities`].
352    #[serde(default)]
353    pub capabilities: Option<Capabilities>,
354
355    /// Package type (contract or agent)
356    pub pkg_type: PkgType,
357
358    /// Package metadata
359    #[serde(default)]
360    pub meta: PkgMeta,
361}
362
363impl WasmPkgNoSource {
364    pub fn into_dto(self) -> WasmPkgNoSourceDto {
365        self.into()
366    }
367}
368
369// TODO: Use json-proof package here
370// TODO: Or - do we need this ? we could simply sign the Vec<u8> of the "WasmPkg" and call it a day.
371//       I think the signing is also only a thing for the registries, because when sending introductions with inline code definition,
372//       the message is always signed by our p2p protocols..
373/// A signed wasm package
374///
375/// The signature is generated, by first generating the json-proof for the [`WasmPkg`] and then signing it with some private-key.
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct WasmPkgSigned {
378    /// Package definition
379    #[serde(flatten)]
380    pub pkg: WasmPkg,
381
382    /// Base-16 encoded signature
383    pub signature: String,
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn source_deserialize() {
392        let s = r#"{
393            "version": "1.2.3",
394            "digest": "",
395            "wasm": "AGFzbQEAAAABnAIqYAF/"
396        }"#;
397        let source: Result<Source, _> = serde_json::from_str(s);
398        assert!(source.is_ok());
399    }
400}