Skip to main content

aleph_types/message/
program.rs

1use crate::item_hash::ItemHash;
2use crate::message::execution::base::{Encoding, ExecutableContent, Interface};
3use crate::message::execution::environment::{FunctionEnvironment, FunctionTriggers};
4use crate::toolkit::serde::{default_some_false, default_true};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct FunctionRuntime {
10    #[serde(rename = "ref")]
11    pub reference: ItemHash,
12    #[serde(default = "default_true")]
13    pub use_latest: bool,
14    pub comment: String,
15}
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct CodeContent {
19    pub encoding: Encoding,
20    pub entrypoint: String,
21    /// Reference to the STORE message containing the code.
22    #[serde(rename = "ref")]
23    pub reference: ItemHash,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub interface: Option<Interface>,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub args: Option<Vec<String>>,
28    #[serde(default)]
29    pub use_latest: bool,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct DataContent {
34    pub encoding: Encoding,
35    pub mount: PathBuf,
36    #[serde(rename = "ref")]
37    pub reference: ItemHash,
38    #[serde(
39        default = "default_some_false",
40        skip_serializing_if = "Option::is_none"
41    )]
42    pub use_latest: Option<bool>,
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct Export {
47    pub encoding: Encoding,
48    pub mount: PathBuf,
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52pub struct ProgramContent {
53    #[serde(flatten)]
54    pub base: ExecutableContent,
55    /// Code to execute.
56    pub code: CodeContent,
57    /// Execution runtime (rootfs with Python interpreter).
58    pub runtime: FunctionRuntime,
59    /// Data to use during computation.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub data: Option<DataContent>,
62    /// Properties of the execution environment.
63    pub environment: FunctionEnvironment,
64    /// Data to export after computation.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub export: Option<Export>,
67    /// Signals that trigger an execution.
68    pub on: FunctionTriggers,
69}
70
71impl ProgramContent {
72    pub fn executable_content(&self) -> &ExecutableContent {
73        &self.base
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::chain::{Address, Chain, Signature};
81    use crate::message::base_message::MessageContentEnum;
82    use crate::message::execution::environment::MachineResources;
83    use crate::message::execution::volume::{BaseVolume, ImmutableVolume, MachineVolume};
84    use crate::message::{ContentSource, Message, MessageType};
85    use crate::timestamp::Timestamp;
86    use crate::{channel, item_hash};
87    use assert_matches::assert_matches;
88    use memsizes::MiB;
89    use std::collections::HashMap;
90
91    const PROGRAM_FIXTURE: &str = include_str!(concat!(
92        env!("CARGO_MANIFEST_DIR"),
93        "/../../fixtures/messages/program/program.json"
94    ));
95
96    const PROGRAM_WITH_EMPTY_ARRAY_AS_METADATA: &str = include_str!(concat!(
97        env!("CARGO_MANIFEST_DIR"),
98        "/../../fixtures/messages/program/program-with-array-as-metadata.json"
99    ));
100
101    #[test]
102    fn test_deserialize_program_message() {
103        let message: Message = serde_json::from_str(PROGRAM_FIXTURE).unwrap();
104
105        assert_eq!(
106            message.sender,
107            Address::from("0x9C2FD74F9CA2B7C4941690316B0Ebc35ce55c885".to_string())
108        );
109        assert_eq!(message.chain, Chain::Ethereum);
110        assert_eq!(
111            message.signature,
112            Signature::from(
113                "0x421c656709851fba752f323a117bc7a07f175a4dd7faf1d8fc1cd9a99028081a6419f9e8b0a7cd454bfef1c52d1f0675a7a59a7d07eb4ebdb22e18bbaf415f881c".to_string()
114            )
115        );
116        assert_matches!(message.message_type, MessageType::Program);
117        assert_matches!(
118            message.content_source,
119            ContentSource::Inline { item_content: _ }
120        );
121        assert_eq!(
122            &message.item_hash.to_string(),
123            "acab01087137c68a5e84734e75145482651accf3bea80fb9b723b761639ecc1c"
124        );
125        assert_eq!(message.time, Timestamp::from(1757026128.773));
126        assert_eq!(message.channel, Some(channel!("ALEPH-CLOUDSOLUTIONS")));
127
128        // Check content fields
129        assert_eq!(
130            &message.content.address,
131            &Address::from("0x9C2FD74F9CA2B7C4941690316B0Ebc35ce55c885".to_string())
132        );
133        assert_eq!(&message.content.time, &Timestamp::from(1757026128.773));
134        assert_eq!(message.sent_at(), &message.content.time);
135
136        // Check program content fields
137        let program_content = match message.content() {
138            MessageContentEnum::Program(content) => content,
139            other => {
140                panic!("Expected MessageContentEnum::Program, got {:?}", other);
141            }
142        };
143
144        assert!(!program_content.base.allow_amend);
145        assert_eq!(
146            program_content.base.metadata,
147            Some(HashMap::from([(
148                "name".to_string(),
149                serde_json::Value::String("Hoymiles".to_string())
150            )]))
151        );
152        assert_eq!(program_content.base.variables, Some(HashMap::new()));
153        assert_eq!(
154            program_content.base.resources,
155            MachineResources {
156                vcpus: 2,
157                memory: MiB::from(4096),
158                seconds: 30,
159                published_ports: None,
160            }
161        );
162        assert_matches!(program_content.base.authorized_keys, None);
163        assert_eq!(
164            program_content.environment,
165            FunctionEnvironment {
166                reproducible: false,
167                internet: true,
168                aleph_api: true,
169                shared_cache: false,
170            }
171        );
172        assert_eq!(
173            program_content.base.volumes,
174            vec![MachineVolume::Immutable(ImmutableVolume {
175                base: BaseVolume {
176                    comment: None,
177                    mount: Some(PathBuf::from("/opt/packages"))
178                },
179                reference: item_hash!(
180                    "8df728d560ed6e9103b040a6b5fc5417e0a52e890c12977464ebadf9becf1bf6"
181                ),
182                use_latest: true,
183            })]
184        );
185        assert_eq!(program_content.base.replaces, None);
186        assert_eq!(
187            program_content.code,
188            CodeContent {
189                encoding: Encoding::Zip,
190                entrypoint: "main:app".to_string(),
191                reference: item_hash!(
192                    "9a4735bca0d3f7032ddd6659c35387b57b470550c931841e6862ece4e9e6523e"
193                ),
194                interface: None,
195                args: None,
196                use_latest: true,
197            }
198        );
199        assert_eq!(
200            program_content.runtime,
201            FunctionRuntime {
202                reference: item_hash!(
203                    "63f07193e6ee9d207b7d1fcf8286f9aee34e6f12f101d2ec77c1229f92964696"
204                ),
205                use_latest: true,
206                comment: "Aleph Alpine Linux with Python 3.12".to_string(),
207            }
208        );
209        assert_eq!(program_content.data, None);
210        assert_eq!(program_content.export, None);
211        assert_eq!(
212            program_content.on,
213            FunctionTriggers {
214                http: true,
215                persistent: Some(false)
216            }
217        );
218
219        // No confirmation on this fixture
220        assert!(!message.confirmed());
221        assert!(message.confirmed_at().is_none());
222        assert!(message.confirmations.is_empty());
223
224        message.verify_item_hash().unwrap();
225    }
226
227    #[test]
228    /// Some nodes return old PROGRAM messages where the metadata field is an empty list instead of
229    /// an object. While this should never happen, fixing this server-side is tricky so we support
230    /// it in the SDK by treating it like an empty map.
231    fn load_program_with_empty_array_as_metadata() {
232        let message: Message = serde_json::from_str(PROGRAM_WITH_EMPTY_ARRAY_AS_METADATA).unwrap();
233
234        // Check that the metadata field is empty
235        let program_content = match message.content() {
236            MessageContentEnum::Program(content) => content,
237            other => {
238                panic!("Expected MessageContentEnum::Program, got {:?}", other);
239            }
240        };
241
242        assert_matches!(program_content.base.metadata, Some(ref map) if map.is_empty());
243    }
244}