1use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use super::error::{ProtocolContractError, ProtocolResult};
8use super::shape::*;
9use crate::{
10 hash_jcs, Compiler, HashError, HashId, ProgramRelease, ProgramSpec, ProtocolHandlerBundle,
11 ProtocolRelease, ProtocolReleaseReview, ProtocolResource, ProtocolSpec, ProtocolStackSource,
12 SdkExtension, StackManifest,
13};
14
15pub const PROTOCOL_STACK_SOURCE_SCHEMA_V1: &str = "arete.protocol-stack-source/v1";
16pub const PROTOCOL_RELEASE_SCHEMA_V1: &str = "arete.protocol-release/v1";
17pub const PROTOCOL_RELEASE_REVIEW_SCHEMA_V1: &str = "arete.protocol-release-review/v1";
18pub const PROTOCOL_BUILD_RECEIPT_SCHEMA_V1: &str = "arete.protocol-build-receipt/v1";
19pub const PROTOCOL_RESOURCE_SCHEMA_V1: &str = "arete.protocol-resource/v1";
20pub const STACK_SOURCE_SCHEMA_V1: &str = "arete.stack-source/v1";
21
22const MAX_DEPENDENCIES: usize = 64;
23
24fn model<T: serde::de::DeserializeOwned>(value: &Value) -> ProtocolResult<T> {
25 serde_json::from_value(value.clone()).map_err(|error| {
26 ProtocolContractError::new("invalid-value", "", format!("model mismatch: {error}"))
27 })
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36pub struct ProtocolStackSourceV1 {
37 pub schema: String,
38 pub generator: GeneratorV1,
39 pub profile: StackProfile,
40 pub stack_source: Value,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase", deny_unknown_fields)]
47pub struct GeneratorV1 {
48 pub name: String,
49 pub version: String,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub enum StackProfile {
55 Interim,
57 Full,
59}
60
61impl ProtocolStackSourceV1 {
62 pub fn hash(&self) -> Result<HashId<ProtocolStackSource>, HashError> {
63 hash_jcs::<ProtocolStackSource, _>(self)
64 }
65}
66
67pub fn validate_protocol_stack_source_value(
68 value: &Value,
69) -> ProtocolResult<ProtocolStackSourceV1> {
70 reject_floats(value, "")?;
71 let root = Obj::new(
72 value,
73 "",
74 &["schema", "generator", "profile", "stackSource"],
75 )?;
76 schema_tag(
77 root.req("schema")?,
78 "/schema",
79 PROTOCOL_STACK_SOURCE_SCHEMA_V1,
80 )?;
81 shape_generator(root.req("generator")?, "/generator")?;
82 one_of(root.req("profile")?, "/profile", &["interim", "full"])?;
83 let source = root.req("stackSource")?;
84 let source_obj = source
85 .as_object()
86 .ok_or_else(|| err("invalid-type", "/stackSource", "expected an object"))?;
87 match source_obj.get("schema") {
88 Some(tag) => schema_tag(tag, "/stackSource/schema", STACK_SOURCE_SCHEMA_V1)?,
89 None => {
90 return Err(err(
91 "missing-field",
92 "/stackSource/schema",
93 "missing field 'schema'",
94 ))
95 }
96 }
97 model(value)
98}
99
100fn shape_generator(value: &Value, path: &str) -> ProtocolResult<()> {
101 let generator = Obj::new(value, path, &["name", "version"])?;
102 let name = bounded_str(generator.req("name")?, &generator.path_of("name"), 128)?;
103 if name.is_empty() {
104 return Err(err(
105 "invalid-value",
106 &generator.path_of("name"),
107 "name must not be empty",
108 ));
109 }
110 semver(generator.req("version")?, &generator.path_of("version"))?;
111 Ok(())
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase", deny_unknown_fields)]
120pub struct ProtocolReleaseV1 {
121 pub schema: String,
122 pub protocol: String,
123 pub version: String,
124 pub spec_hash: HashId<ProtocolSpec>,
125 pub handler_bundle_hash: Option<HashId<ProtocolHandlerBundle>>,
127 pub stack_source_hash: HashId<ProtocolStackSource>,
128 pub compiler_hash: HashId<Compiler>,
129 pub dependencies: ReleaseDependenciesV1,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase", deny_unknown_fields)]
134pub struct ReleaseDependenciesV1 {
135 pub programs: Vec<ProgramDependencyV1>,
136 pub stacks: Vec<StackDependencyV1>,
137 pub sdk_extensions: Vec<SdkExtensionDependencyV1>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase", deny_unknown_fields)]
142pub struct ProgramDependencyV1 {
143 pub alias: String,
144 pub program_id: String,
145 pub program_spec_hash: HashId<ProgramSpec>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub program_release_hash: Option<HashId<ProgramRelease>>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase", deny_unknown_fields)]
152pub struct StackDependencyV1 {
153 pub alias: String,
154 pub stack_manifest_hash: HashId<StackManifest>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase", deny_unknown_fields)]
159pub struct SdkExtensionDependencyV1 {
160 pub alias: String,
161 pub sdk_extension_hash: HashId<SdkExtension>,
162}
163
164impl ProtocolReleaseV1 {
165 pub fn hash(&self) -> Result<HashId<ProtocolRelease>, HashError> {
166 hash_jcs::<ProtocolRelease, _>(self)
167 }
168}
169
170pub fn validate_protocol_release_value(value: &Value) -> ProtocolResult<ProtocolReleaseV1> {
171 reject_floats(value, "")?;
172 let root = Obj::new(
173 value,
174 "",
175 &[
176 "schema",
177 "protocol",
178 "version",
179 "specHash",
180 "handlerBundleHash",
181 "stackSourceHash",
182 "compilerHash",
183 "dependencies",
184 ],
185 )?;
186 schema_tag(root.req("schema")?, "/schema", PROTOCOL_RELEASE_SCHEMA_V1)?;
187 protocol_name(as_str(root.req("protocol")?, "/protocol")?, "/protocol")?;
188 semver(root.req("version")?, "/version")?;
189 hash_id::<ProtocolSpec>(root.req("specHash")?, "/specHash")?;
190 if let Some(value) = root.nullable("handlerBundleHash")? {
191 hash_id::<ProtocolHandlerBundle>(value, "/handlerBundleHash")?;
192 }
193 hash_id::<ProtocolStackSource>(root.req("stackSourceHash")?, "/stackSourceHash")?;
194 hash_id::<Compiler>(root.req("compilerHash")?, "/compilerHash")?;
195 let deps = Obj::new(
196 root.req("dependencies")?,
197 "/dependencies",
198 &["programs", "stacks", "sdkExtensions"],
199 )?;
200 sorted_entries(
201 deps.req("programs")?,
202 "/dependencies/programs",
203 |entry, path| {
204 let entry = Obj::new(
205 entry,
206 path,
207 &[
208 "alias",
209 "programId",
210 "programSpecHash",
211 "programReleaseHash",
212 ],
213 )?;
214 let alias = identifier_value(entry.req("alias")?, &entry.path_of("alias"))?;
215 pubkey(entry.req("programId")?, &entry.path_of("programId"))?;
216 hash_id::<ProgramSpec>(
217 entry.req("programSpecHash")?,
218 &entry.path_of("programSpecHash"),
219 )?;
220 if let Some(value) = entry.opt("programReleaseHash")? {
221 hash_id::<ProgramRelease>(value, &entry.path_of("programReleaseHash"))?;
222 }
223 Ok(alias.to_string())
224 },
225 )?;
226 sorted_entries(
227 deps.req("stacks")?,
228 "/dependencies/stacks",
229 |entry, path| {
230 let entry = Obj::new(entry, path, &["alias", "stackManifestHash"])?;
231 let alias = identifier_value(entry.req("alias")?, &entry.path_of("alias"))?;
232 hash_id::<StackManifest>(
233 entry.req("stackManifestHash")?,
234 &entry.path_of("stackManifestHash"),
235 )?;
236 Ok(alias.to_string())
237 },
238 )?;
239 sorted_entries(
240 deps.req("sdkExtensions")?,
241 "/dependencies/sdkExtensions",
242 |entry, path| {
243 let entry = Obj::new(entry, path, &["alias", "sdkExtensionHash"])?;
244 let alias = identifier_value(entry.req("alias")?, &entry.path_of("alias"))?;
245 hash_id::<SdkExtension>(
246 entry.req("sdkExtensionHash")?,
247 &entry.path_of("sdkExtensionHash"),
248 )?;
249 Ok(alias.to_string())
250 },
251 )?;
252 model(value)
253}
254
255pub(crate) fn sorted_entries(
258 value: &Value,
259 path: &str,
260 entry: impl Fn(&Value, &str) -> ProtocolResult<String>,
261) -> ProtocolResult<()> {
262 let values = as_array(value, path, MAX_DEPENDENCIES)?;
263 let mut previous: Option<String> = None;
264 for (i, item) in values.iter().enumerate() {
265 let item_path = index(path, i);
266 let key = entry(item, &item_path)?;
267 if let Some(previous) = &previous {
268 if key == *previous {
269 return Err(err(
270 "duplicate-entry",
271 &item_path,
272 format!("'{key}' is listed twice"),
273 ));
274 }
275 if key < *previous {
276 return Err(err(
277 "unsorted-entries",
278 &item_path,
279 "entries must be sorted",
280 ));
281 }
282 }
283 previous = Some(key);
284 }
285 Ok(())
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase", deny_unknown_fields)]
294pub struct ProtocolReleaseReviewV1 {
295 pub schema: String,
296 pub release_hash: HashId<ProtocolRelease>,
297 pub review_key_id: String,
298 pub trust_tier: TrustTier,
299 pub signature: String,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub enum TrustTier {
316 Reviewed,
317 Unreviewed,
318}
319
320impl TrustTier {
321 pub const ALL: [&'static str; 2] = ["reviewed", "unreviewed"];
322
323 pub const fn as_str(self) -> &'static str {
324 match self {
325 Self::Reviewed => "reviewed",
326 Self::Unreviewed => "unreviewed",
327 }
328 }
329
330 pub fn parse(value: &str) -> Option<Self> {
331 match value {
332 "reviewed" => Some(Self::Reviewed),
333 "unreviewed" => Some(Self::Unreviewed),
334 _ => None,
335 }
336 }
337}
338
339impl ProtocolReleaseReviewV1 {
340 pub fn hash(&self) -> Result<HashId<ProtocolReleaseReview>, HashError> {
341 hash_jcs::<ProtocolReleaseReview, _>(self)
342 }
343
344 pub fn signing_message(&self) -> Vec<u8> {
345 review_signing_message(&self.release_hash, &self.review_key_id, self.trust_tier)
346 }
347
348 pub fn signature_bytes(&self) -> [u8; 64] {
350 let mut out = [0u8; 64];
351 if let Some(bytes) = decode_base64(&self.signature) {
352 if bytes.len() == 64 {
353 out.copy_from_slice(&bytes);
354 }
355 }
356 out
357 }
358}
359
360pub fn review_signing_message(
364 release_hash: &HashId<ProtocolRelease>,
365 review_key_id: &str,
366 trust_tier: TrustTier,
367) -> Vec<u8> {
368 format!(
369 "{PROTOCOL_RELEASE_REVIEW_SCHEMA_V1}\n{review_key_id}\n{}\n{release_hash}",
370 trust_tier.as_str()
371 )
372 .into_bytes()
373}
374
375pub fn validate_protocol_release_review_value(
376 value: &Value,
377) -> ProtocolResult<ProtocolReleaseReviewV1> {
378 reject_floats(value, "")?;
379 let root = Obj::new(
380 value,
381 "",
382 &[
383 "schema",
384 "releaseHash",
385 "reviewKeyId",
386 "trustTier",
387 "signature",
388 ],
389 )?;
390 schema_tag(
391 root.req("schema")?,
392 "/schema",
393 PROTOCOL_RELEASE_REVIEW_SCHEMA_V1,
394 )?;
395 hash_id::<ProtocolRelease>(root.req("releaseHash")?, "/releaseHash")?;
396 key_id(root.req("reviewKeyId")?, "/reviewKeyId")?;
397 one_of(root.req("trustTier")?, "/trustTier", &["reviewed"])?;
399 let signature = as_str(root.req("signature")?, "/signature")?;
400 match decode_base64(signature) {
401 Some(bytes) if bytes.len() == 64 => {}
402 _ => {
403 return Err(err(
404 "invalid-signature",
405 "/signature",
406 "expected a base64 64-byte Ed25519 signature",
407 ))
408 }
409 }
410 model(value)
411}
412
413pub(crate) fn key_id<'a>(value: &'a Value, path: &str) -> ProtocolResult<&'a str> {
415 let text = as_str(value, path)?;
416 let bytes = text.as_bytes();
417 let valid = !bytes.is_empty()
418 && bytes.len() <= 64
419 && (bytes[0].is_ascii_lowercase() || bytes[0].is_ascii_digit())
420 && bytes
421 .iter()
422 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || *b == b'-');
423 if valid {
424 Ok(text)
425 } else {
426 Err(err(
427 "invalid-identifier",
428 path,
429 "key ids match ^[a-z0-9][a-z0-9-]{0,63}$",
430 ))
431 }
432}
433
434const BASE64_ALPHABET: &[u8; 64] =
435 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
436
437pub fn decode_base64(text: &str) -> Option<Vec<u8>> {
440 let bytes = text.as_bytes();
441 if !bytes.len().is_multiple_of(4) {
442 return None;
443 }
444 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
445 for (chunk_index, chunk) in bytes.chunks(4).enumerate() {
446 let last = chunk_index == bytes.len() / 4 - 1;
447 let mut values = [0u8; 4];
448 let mut padding = 0;
449 for (i, byte) in chunk.iter().enumerate() {
450 if *byte == b'=' {
451 if !last || i < 2 {
452 return None;
453 }
454 padding += 1;
455 values[i] = 0;
456 } else {
457 if padding > 0 {
458 return None;
459 }
460 values[i] = BASE64_ALPHABET.iter().position(|c| c == byte)? as u8;
461 }
462 }
463 let n = (u32::from(values[0]) << 18)
464 | (u32::from(values[1]) << 12)
465 | (u32::from(values[2]) << 6)
466 | u32::from(values[3]);
467 out.push((n >> 16) as u8);
468 if padding < 2 {
469 out.push((n >> 8) as u8);
470 }
471 if padding < 1 {
472 out.push(n as u8);
473 }
474 if (padding == 1 && values[2] & 0b11 != 0) || (padding == 2 && values[1] & 0b1111 != 0) {
476 return None;
477 }
478 }
479 Some(out)
480}
481
482pub fn encode_base64(bytes: &[u8]) -> String {
483 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
484 for chunk in bytes.chunks(3) {
485 let n = (u32::from(chunk[0]) << 16)
486 | (u32::from(*chunk.get(1).unwrap_or(&0)) << 8)
487 | u32::from(*chunk.get(2).unwrap_or(&0));
488 out.push(BASE64_ALPHABET[(n >> 18) as usize & 63] as char);
489 out.push(BASE64_ALPHABET[(n >> 12) as usize & 63] as char);
490 out.push(if chunk.len() > 1 {
491 BASE64_ALPHABET[(n >> 6) as usize & 63] as char
492 } else {
493 '='
494 });
495 out.push(if chunk.len() > 2 {
496 BASE64_ALPHABET[n as usize & 63] as char
497 } else {
498 '='
499 });
500 }
501 out
502}
503
504#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
509#[serde(rename_all = "camelCase", deny_unknown_fields)]
510pub struct ProtocolBuildReceiptV1 {
511 pub schema: String,
512 pub source_tree_sha256: String,
513 pub lockfile_sha256: String,
514 pub dependencies: Vec<BuildDependencyV1>,
515 pub compiler: BuildCompilerV1,
516 pub outputs: BuildOutputsV1,
517}
518
519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
520#[serde(rename_all = "camelCase", deny_unknown_fields)]
521pub struct BuildDependencyV1 {
522 pub name: String,
523 pub version: String,
524 pub integrity: String,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
528#[serde(rename_all = "camelCase", deny_unknown_fields)]
529pub struct BuildCompilerV1 {
530 pub name: String,
531 pub version: String,
532 pub esbuild: String,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(rename_all = "camelCase", deny_unknown_fields)]
537pub struct BuildOutputsV1 {
538 pub spec_hash: HashId<ProtocolSpec>,
539 pub handler_bundle_hash: Option<HashId<ProtocolHandlerBundle>>,
540 pub stack_source_hash: HashId<ProtocolStackSource>,
541 pub release_hash: HashId<ProtocolRelease>,
542}
543
544pub fn validate_protocol_build_receipt_value(
545 value: &Value,
546) -> ProtocolResult<ProtocolBuildReceiptV1> {
547 reject_floats(value, "")?;
548 let root = Obj::new(
549 value,
550 "",
551 &[
552 "schema",
553 "sourceTreeSha256",
554 "lockfileSha256",
555 "dependencies",
556 "compiler",
557 "outputs",
558 ],
559 )?;
560 schema_tag(
561 root.req("schema")?,
562 "/schema",
563 PROTOCOL_BUILD_RECEIPT_SCHEMA_V1,
564 )?;
565 sha256_hex(root.req("sourceTreeSha256")?, "/sourceTreeSha256")?;
566 sha256_hex(root.req("lockfileSha256")?, "/lockfileSha256")?;
567 sorted_entries(root.req("dependencies")?, "/dependencies", |entry, path| {
568 let entry = Obj::new(entry, path, &["name", "version", "integrity"])?;
569 let name = bounded_str(entry.req("name")?, &entry.path_of("name"), 214)?;
570 bounded_str(entry.req("version")?, &entry.path_of("version"), 64)?;
571 bounded_str(entry.req("integrity")?, &entry.path_of("integrity"), 256)?;
572 Ok(name.to_string())
573 })?;
574 let compiler = Obj::new(
575 root.req("compiler")?,
576 "/compiler",
577 &["name", "version", "esbuild"],
578 )?;
579 bounded_str(compiler.req("name")?, "/compiler/name", 128)?;
580 semver(compiler.req("version")?, "/compiler/version")?;
581 semver(compiler.req("esbuild")?, "/compiler/esbuild")?;
582 let outputs = Obj::new(
583 root.req("outputs")?,
584 "/outputs",
585 &[
586 "specHash",
587 "handlerBundleHash",
588 "stackSourceHash",
589 "releaseHash",
590 ],
591 )?;
592 hash_id::<ProtocolSpec>(outputs.req("specHash")?, "/outputs/specHash")?;
593 if let Some(value) = outputs.nullable("handlerBundleHash")? {
594 hash_id::<ProtocolHandlerBundle>(value, "/outputs/handlerBundleHash")?;
595 }
596 hash_id::<ProtocolStackSource>(outputs.req("stackSourceHash")?, "/outputs/stackSourceHash")?;
597 hash_id::<ProtocolRelease>(outputs.req("releaseHash")?, "/outputs/releaseHash")?;
598 model(value)
599}
600
601#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
606#[serde(rename_all = "camelCase", deny_unknown_fields)]
607pub struct ProtocolResourceV1 {
608 pub schema: String,
609 pub name: String,
610 pub schema_sha256: String,
613 pub value: Value,
614}
615
616impl ProtocolResourceV1 {
617 pub fn hash(&self) -> Result<HashId<ProtocolResource>, HashError> {
618 hash_jcs::<ProtocolResource, _>(self)
619 }
620}
621
622pub fn validate_protocol_resource_value(value: &Value) -> ProtocolResult<ProtocolResourceV1> {
623 reject_floats(value, "")?;
624 let root = Obj::new(value, "", &["schema", "name", "schemaSha256", "value"])?;
625 schema_tag(root.req("schema")?, "/schema", PROTOCOL_RESOURCE_SCHEMA_V1)?;
626 identifier_value(root.req("name")?, "/name")?;
627 sha256_hex(root.req("schemaSha256")?, "/schemaSha256")?;
628 root.req("value")?;
629 model(value)
630}