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}