1use sha2::{Digest, Sha256};
26
27use super::hash_handler_bundle;
28use super::model::{ProtocolProgramV1, ProtocolSpecV1};
29use super::release::{
30 BuildCompilerV1, BuildOutputsV1, GeneratorV1, ProgramDependencyV1, ProtocolBuildReceiptV1,
31 ProtocolReleaseV1, ProtocolStackSourceV1, ReleaseDependenciesV1, StackDependencyV1,
32 PROTOCOL_BUILD_RECEIPT_SCHEMA_V1, PROTOCOL_RELEASE_SCHEMA_V1,
33};
34use super::stack::{generate_stack_source, StackGenerationError, StackSourceOptions};
35use crate::{
36 canonicalize_jcs, Compiler, CompilerSourceV1, CompilerV1, HashError, HashId,
37 ProtocolHandlerBundle, ProtocolRelease, ProtocolSpec, ProtocolStackSource,
38};
39
40pub const SERVER_COMPILER_NAME: &str = "arete-protocol-server";
42
43pub const SERVER_DERIVATION_VERSION: &str = "1.0.0";
47
48pub const SERVER_COMPILER_ESBUILD: &str = "0.0.0";
51
52const EMPTY_SHA256: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
55
56#[derive(Debug, thiserror::Error)]
58pub enum ReleaseDerivationError {
59 #[error("the spec has handler steps but no handler bundle was supplied")]
60 MissingBundle,
61 #[error("a template-only spec cannot carry a handler bundle")]
62 UnexpectedBundle,
63 #[error("the handler bundle is empty")]
64 EmptyBundle,
65 #[error("the stack source cannot be generated: {0}")]
66 Stack(#[from] StackGenerationError),
67 #[error("a derived document cannot be hashed: {0}")]
68 Hash(#[from] HashError),
69}
70
71impl ReleaseDerivationError {
72 pub fn code(&self) -> &'static str {
74 match self {
75 Self::MissingBundle | Self::UnexpectedBundle | Self::EmptyBundle => {
76 "protocol-bundle-mismatch"
77 }
78 Self::Stack(_) => "protocol-stack-generation-failed",
79 Self::Hash(_) => "protocol-invalid-document",
80 }
81 }
82}
83
84impl BuildCompilerV1 {
85 pub fn compiler_hash(&self) -> Result<HashId<Compiler>, HashError> {
89 CompilerV1::new([
90 CompilerSourceV1::new("esbuild", self.esbuild.as_bytes()),
91 CompilerSourceV1::new("name", self.name.as_bytes()),
92 CompilerSourceV1::new("version", self.version.as_bytes()),
93 ])?
94 .hash()
95 }
96}
97
98pub fn server_compiler() -> BuildCompilerV1 {
100 BuildCompilerV1 {
101 name: SERVER_COMPILER_NAME.to_string(),
102 version: SERVER_DERIVATION_VERSION.to_string(),
103 esbuild: SERVER_COMPILER_ESBUILD.to_string(),
104 }
105}
106
107pub fn server_stack_generator() -> GeneratorV1 {
109 GeneratorV1 {
110 name: SERVER_COMPILER_NAME.to_string(),
111 version: SERVER_DERIVATION_VERSION.to_string(),
112 }
113}
114
115pub fn server_stack_source_options() -> StackSourceOptions {
118 StackSourceOptions {
119 generator: Some(server_stack_generator()),
120 ..StackSourceOptions::default()
121 }
122}
123
124pub fn release_dependencies(spec: &ProtocolSpecV1) -> ReleaseDependenciesV1 {
127 let programs = spec
129 .programs
130 .iter()
131 .filter_map(|(alias, program)| match program {
132 ProtocolProgramV1::ProgramSpec {
133 program_id,
134 program_spec_hash,
135 program_release_hash,
136 } => Some(ProgramDependencyV1 {
137 alias: alias.clone(),
138 program_id: program_id.clone(),
139 program_spec_hash: *program_spec_hash,
140 program_release_hash: *program_release_hash,
141 }),
142 ProtocolProgramV1::Builtin { .. } => None,
143 })
144 .collect();
145 let stacks = spec
146 .stacks
147 .iter()
148 .map(|(alias, stack)| StackDependencyV1 {
149 alias: alias.clone(),
150 stack_manifest_hash: stack.stack_manifest_hash,
151 })
152 .collect();
153 ReleaseDependenciesV1 {
154 programs,
155 stacks,
156 sdk_extensions: Vec::new(),
157 }
158}
159
160fn bundle_hash(
161 spec: &ProtocolSpecV1,
162 handler_bundle: Option<&[u8]>,
163) -> Result<Option<HashId<ProtocolHandlerBundle>>, ReleaseDerivationError> {
164 match (spec.has_handlers(), handler_bundle) {
165 (true, Some([])) => Err(ReleaseDerivationError::EmptyBundle),
166 (true, Some(bytes)) => Ok(Some(hash_handler_bundle(bytes)?)),
167 (true, None) => Err(ReleaseDerivationError::MissingBundle),
168 (false, Some(_)) => Err(ReleaseDerivationError::UnexpectedBundle),
169 (false, None) => Ok(None),
170 }
171}
172
173pub fn derive_release(
176 spec: &ProtocolSpecV1,
177 handler_bundle: Option<&[u8]>,
178 stack_source: &ProtocolStackSourceV1,
179 compiler: &BuildCompilerV1,
180) -> Result<ProtocolReleaseV1, ReleaseDerivationError> {
181 Ok(ProtocolReleaseV1 {
182 schema: PROTOCOL_RELEASE_SCHEMA_V1.to_string(),
183 protocol: spec.name.clone(),
184 version: spec.version.clone(),
185 spec_hash: spec.hash()?,
186 handler_bundle_hash: bundle_hash(spec, handler_bundle)?,
187 stack_source_hash: stack_source.hash()?,
188 compiler_hash: compiler.compiler_hash()?,
189 dependencies: release_dependencies(spec),
190 })
191}
192
193#[derive(Debug, Clone, PartialEq)]
195pub struct ServerDerivedRelease {
196 pub spec_hash: HashId<ProtocolSpec>,
197 pub handler_bundle_hash: Option<HashId<ProtocolHandlerBundle>>,
198 pub stack_source: ProtocolStackSourceV1,
199 pub stack_source_hash: HashId<ProtocolStackSource>,
200 pub release: ProtocolReleaseV1,
201 pub release_hash: HashId<ProtocolRelease>,
202 pub receipt: ProtocolBuildReceiptV1,
206}
207
208fn sha256_hex(bytes: &[u8]) -> String {
209 hex::encode(Sha256::digest(bytes))
210}
211
212pub fn derive_server_release(
216 spec: &ProtocolSpecV1,
217 handler_bundle: Option<&[u8]>,
218) -> Result<ServerDerivedRelease, ReleaseDerivationError> {
219 let handler_bundle_hash = bundle_hash(spec, handler_bundle)?;
220 let stack_source = generate_stack_source(spec, &server_stack_source_options())?;
221 let compiler = server_compiler();
222 let release = derive_release(spec, handler_bundle, &stack_source, &compiler)?;
223 let release_hash = release.hash()?;
224 let spec_hash = release.spec_hash;
225 let stack_source_hash = release.stack_source_hash;
226
227 let mut tree: Vec<(String, String)> = Vec::with_capacity(2);
230 if let Some(bytes) = handler_bundle {
231 tree.push(("handlers.mjs".into(), sha256_hex(bytes)));
232 }
233 tree.push((
234 "protocol-spec.json".into(),
235 sha256_hex(&canonicalize_jcs(spec)?),
236 ));
237 let source_tree_sha256 = sha256_hex(&canonicalize_jcs(&tree)?);
238
239 let receipt = ProtocolBuildReceiptV1 {
240 schema: PROTOCOL_BUILD_RECEIPT_SCHEMA_V1.to_string(),
241 source_tree_sha256,
242 lockfile_sha256: EMPTY_SHA256.to_string(),
243 dependencies: Vec::new(),
244 compiler,
245 outputs: BuildOutputsV1 {
246 spec_hash,
247 handler_bundle_hash,
248 stack_source_hash,
249 release_hash,
250 },
251 };
252 Ok(ServerDerivedRelease {
253 spec_hash,
254 handler_bundle_hash,
255 stack_source,
256 stack_source_hash,
257 release,
258 release_hash,
259 receipt,
260 })
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn the_empty_lockfile_digest_is_sha256_of_nothing() {
269 assert_eq!(sha256_hex(&[]), EMPTY_SHA256);
270 }
271
272 #[test]
273 fn the_server_compiler_is_a_valid_receipt_compiler() {
274 let compiler = server_compiler();
275 assert_eq!(compiler.name, SERVER_COMPILER_NAME);
276 compiler.compiler_hash().unwrap();
277 }
278}