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