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