exocore_core/cell/
app.rs

1use std::{path::Path, sync::Arc};
2
3use exocore_protos::{
4    generated::exocore_apps::{manifest_schema::Source, Manifest},
5    reflect::{FileDescriptorSet, Message},
6};
7use libp2p::PeerId;
8
9use super::{Error, ManifestExt};
10use crate::{
11    dir::DynDirectory,
12    sec::{
13        hash::{multihash_decode_bs58, multihash_sha3_256, MultihashExt},
14        keys::{Keypair, PublicKey},
15    },
16};
17
18const MANIFEST_FILE_NAME: &str = "app.yaml";
19
20/// Application that extends the capability of the cell by providing schemas and
21/// WebAssembly logic.
22#[derive(Clone)]
23pub struct Application {
24    identity: Arc<Identity>,
25    schemas: Arc<[FileDescriptorSet]>,
26    dir: DynDirectory,
27}
28
29struct Identity {
30    public_key: PublicKey,
31    id: ApplicationId,
32    manifest: Manifest,
33}
34
35impl Application {
36    pub fn generate(
37        dir: impl Into<DynDirectory>,
38        name: String,
39    ) -> Result<(Keypair, Application), Error> {
40        let keypair = Keypair::generate_ed25519();
41        let dir = dir.into();
42
43        let manifest = Manifest {
44            name,
45            public_key: keypair.public().encode_base58_string(),
46            version: "0.0.1".to_string(),
47            ..Default::default()
48        };
49
50        Ok((keypair, Application::from_manifest(dir, manifest)?))
51    }
52
53    pub fn from_directory(dir: impl Into<DynDirectory>) -> Result<Application, Error> {
54        let dir = dir.into();
55
56        let manifest = {
57            let manifest_file = dir.open_read(Path::new(MANIFEST_FILE_NAME))?;
58            Manifest::read_yaml(manifest_file)?
59        };
60
61        Self::from_manifest(dir, manifest)
62    }
63
64    pub fn from_manifest(
65        dir: impl Into<DynDirectory>,
66        manifest: Manifest,
67    ) -> Result<Application, Error> {
68        let dir = dir.into();
69        let public_key = PublicKey::decode_base58_string(&manifest.public_key).map_err(|err| {
70            Error::Application(
71                manifest.name.clone(),
72                anyhow!("Error parsing application public_key: {}", err),
73            )
74        })?;
75
76        let id = ApplicationId::from_public_key(&public_key);
77
78        let mut schemas = Vec::new();
79        for app_schema in &manifest.schemas {
80            match &app_schema.source {
81                Some(Source::File(schema_path)) => {
82                    let schema_file = dir.open_read(Path::new(schema_path))?;
83                    let fd_set = read_file_descriptor_set(&manifest.name, schema_file)?;
84                    schemas.push(fd_set);
85                }
86                other => {
87                    return Err(Error::Application(
88                        manifest.name.clone(),
89                        anyhow!("Unsupported application schema source: {:?}", other),
90                    ));
91                }
92            }
93        }
94
95        Ok(Application {
96            identity: Arc::new(Identity {
97                public_key,
98                id,
99                manifest,
100            }),
101            schemas: schemas.into(),
102            dir,
103        })
104    }
105
106    pub fn public_key(&self) -> &PublicKey {
107        &self.identity.public_key
108    }
109
110    pub fn id(&self) -> &ApplicationId {
111        &self.identity.id
112    }
113
114    pub fn name(&self) -> &str {
115        &self.identity.manifest.name
116    }
117
118    pub fn version(&self) -> &str {
119        &self.identity.manifest.version
120    }
121
122    pub fn manifest(&self) -> &Manifest {
123        &self.identity.manifest
124    }
125
126    pub fn schemas(&self) -> &[FileDescriptorSet] {
127        self.schemas.as_ref()
128    }
129
130    pub fn directory(&self) -> &DynDirectory {
131        &self.dir
132    }
133
134    pub fn validate(&self) -> Result<(), Error> {
135        // validate module
136        if let Some(module) = &self.manifest().module {
137            let module_file = self.directory().open_read(Path::new(&module.file))?;
138
139            let module_multihash = multihash_sha3_256(module_file).map_err(|err| {
140                Error::Application(
141                    self.name().to_string(),
142                    anyhow!("Couldn't multihash module file at: {}", err),
143                )
144            })?;
145
146            let expected_multihash =
147                multihash_decode_bs58::<32>(&module.multihash).map_err(|err| {
148                    Error::Application(
149                        self.name().to_string(),
150                        anyhow!(
151                            "{}: Couldn't decode expected module multihash in manifest: {}",
152                            self.name(),
153                            err
154                        ),
155                    )
156                })?;
157
158            if expected_multihash != module_multihash {
159                return Err(Error::Application(
160                    self.name().to_string(),
161                    anyhow!(
162                        "Module multihash in manifest doesn't match module file (expected={} module={})",
163                        expected_multihash.encode_bs58(),
164                        module_multihash.encode_bs58(),
165                    ),
166                ));
167            }
168        }
169
170        Ok(())
171    }
172
173    pub fn save_manifest(&self, manifest: &Manifest) -> Result<(), Error> {
174        let manifest_file = self.dir.open_write(Path::new(MANIFEST_FILE_NAME))?;
175        manifest.write_yaml(manifest_file)?;
176        Ok(())
177    }
178
179    pub fn manifest_exists(dir: impl Into<DynDirectory>) -> bool {
180        dir.into().exists(Path::new(MANIFEST_FILE_NAME))
181    }
182}
183
184/// Unique identifier of an application, which is built by hashing the public
185/// key.
186///
187/// For now, this ID is generated the same way as node IDs.
188#[derive(PartialEq, Eq, Clone, Debug, Hash)]
189pub struct ApplicationId(String);
190
191impl ApplicationId {
192    /// Create a Cell ID from a public key by using libp2p method to be
193    /// compatible with it
194    pub fn from_public_key(public_key: &PublicKey) -> ApplicationId {
195        let peer_id = PeerId::from_public_key(public_key.to_libp2p());
196        ApplicationId(peer_id.to_string())
197    }
198
199    pub fn from_base58_public_key(public_key: &str) -> Result<ApplicationId, Error> {
200        let public_key = PublicKey::decode_base58_string(public_key)?;
201        Ok(ApplicationId::from_public_key(&public_key))
202    }
203
204    pub fn from_string(id: String) -> ApplicationId {
205        ApplicationId(id)
206    }
207
208    pub fn from_bytes(id: &[u8]) -> ApplicationId {
209        ApplicationId(String::from_utf8_lossy(id).to_string())
210    }
211
212    #[inline]
213    pub fn as_bytes(&self) -> &[u8] {
214        self.0.as_bytes()
215    }
216}
217
218impl std::fmt::Display for ApplicationId {
219    #[inline]
220    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
221        std::fmt::Display::fmt(&self.0, f)
222    }
223}
224
225impl std::str::FromStr for ApplicationId {
226    type Err = ();
227
228    fn from_str(s: &str) -> Result<Self, Self::Err> {
229        Ok(ApplicationId(s.to_string()))
230    }
231}
232
233fn read_file_descriptor_set(
234    app_name: &str,
235    mut content: impl std::io::Read,
236) -> Result<FileDescriptorSet, Error> {
237    let fd_set = FileDescriptorSet::parse_from_reader(&mut content).map_err(|err| {
238        Error::Application(
239            app_name.to_string(),
240            anyhow!(
241                "Couldn't parse application schema file descriptor set: {}",
242                err
243            ),
244        )
245    })?;
246
247    Ok(fd_set)
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::dir::ram::RamDirectory;
254
255    #[test]
256    fn generate_and_validate() -> anyhow::Result<()> {
257        let dir = RamDirectory::new();
258        let (_kp, app) = Application::generate(dir, "some_app".to_string())?;
259        app.validate()?;
260        Ok(())
261    }
262
263    #[test]
264    fn app_id_conversion() {
265        let kp = crate::sec::keys::Keypair::generate_ed25519();
266        let app_id = ApplicationId::from_public_key(&kp.public());
267
268        assert_eq!(app_id, ApplicationId::from_string(app_id.to_string()));
269        assert_eq!(app_id, ApplicationId::from_bytes(app_id.as_bytes()));
270        assert_eq!(
271            app_id,
272            ApplicationId::from_base58_public_key(kp.public().encode_base58_string().as_str())
273                .unwrap()
274        );
275    }
276}