sonicapi/
schema.rs

1// SONIC: Toolchain for formally-verifiable distributed contracts
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Designed in 2019-2025 by Dr Maxim Orlovsky <orlovsky@ubideco.org>
6// Written in 2024-2025 by Dr Maxim Orlovsky <orlovsky@ubideco.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland.
9// Copyright (C) 2024-2025 Laboratories for Ubiquitous Deterministic Computing (UBIDECO),
10//                         Institute for Distributed and Cognitive Systems (InDCS), Switzerland.
11// Copyright (C) 2019-2025 Dr Maxim Orlovsky.
12// All rights under the above copyrights are reserved.
13//
14// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
15// in compliance with the License. You may obtain a copy of the License at
16//
17//        http://www.apache.org/licenses/LICENSE-2.0
18//
19// Unless required by applicable law or agreed to in writing, software distributed under the License
20// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
21// or implied. See the License for the specific language governing permissions and limitations under
22// the License.
23
24use aluvm::{Lib, LibId};
25use amplify::confinement::{SmallOrdMap, SmallOrdSet, TinyOrdMap};
26use commit_verify::ReservedBytes;
27use strict_encoding::{StrictDeserialize, StrictSerialize, TypeName};
28use strict_types::TypeSystem;
29use ultrasonic::{CallId, Codex, LibRepo};
30
31use crate::sigs::ContentSigs;
32use crate::{Annotations, Api, MergeError, MethodName, LIB_NAME_SONIC};
33
34/// Schema contains information required for creation of a contract.
35#[derive(Clone, Eq, PartialEq, Debug)]
36#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
37#[strict_type(lib = LIB_NAME_SONIC)]
38#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
39pub struct Schema {
40    pub codex: Codex,
41    pub default_api: Api,
42    pub default_api_sigs: ContentSigs,
43    pub custom_apis: SmallOrdMap<Api, ContentSigs>,
44    pub libs: SmallOrdSet<Lib>,
45    pub types: TypeSystem,
46    pub codex_sigs: ContentSigs,
47    pub annotations: TinyOrdMap<Annotations, ContentSigs>,
48    pub reserved: ReservedBytes<8>,
49}
50
51impl StrictSerialize for Schema {}
52impl StrictDeserialize for Schema {}
53
54impl LibRepo for Schema {
55    fn get_lib(&self, lib_id: LibId) -> Option<&Lib> { self.libs.iter().find(|lib| lib.lib_id() == lib_id) }
56}
57
58impl Schema {
59    pub fn new(codex: Codex, api: Api, libs: impl IntoIterator<Item = Lib>, types: TypeSystem) -> Self {
60        // TODO: Ensure default API is unnamed?
61        Schema {
62            codex,
63            default_api: api,
64            default_api_sigs: none!(),
65            custom_apis: none!(),
66            libs: SmallOrdSet::from_iter_checked(libs),
67            types,
68            codex_sigs: none!(),
69            annotations: none!(),
70            reserved: zero!(),
71        }
72    }
73
74    pub fn api(&self, name: &TypeName) -> &Api {
75        self.custom_apis
76            .keys()
77            .find(|api| api.name() == Some(name))
78            .expect("API is not known")
79    }
80
81    pub fn call_id(&self, method: impl Into<MethodName>) -> CallId {
82        self.default_api
83            .verifier(method)
84            .expect("calling to method absent in Codex API")
85    }
86
87    pub fn merge(&mut self, other: Self) -> Result<bool, MergeError> {
88        if self.codex.codex_id() != other.codex.codex_id() {
89            return Err(MergeError::CodexMismatch);
90        }
91        self.codex_sigs.merge(other.codex_sigs);
92
93        if self.default_api != other.default_api {
94            let _ = self
95                .custom_apis
96                .insert(other.default_api, other.default_api_sigs);
97        } else {
98            self.default_api_sigs.merge(other.default_api_sigs);
99        }
100
101        for (api, other_sigs) in other.custom_apis {
102            let Ok(entry) = self.custom_apis.entry(api) else {
103                continue;
104            };
105            entry.or_default().merge(other_sigs);
106        }
107
108        // NB: We must not fail here, since otherwise it opens an attack vector on invalidating valid
109        // consignments by adding too many libs
110        // TODO: Return warnings instead
111        let _ = self.libs.extend(other.libs);
112        let _ = self.types.extend(other.types);
113
114        for (annotation, other_sigs) in other.annotations {
115            let Ok(entry) = self.annotations.entry(annotation) else {
116                continue;
117            };
118            entry.or_default().merge(other_sigs);
119        }
120
121        Ok(true)
122    }
123}
124
125#[cfg(feature = "std")]
126mod _fs {
127    use std::path::Path;
128
129    use strict_encoding::{DeserializeError, SerializeError, StrictDeserialize, StrictSerialize};
130
131    use super::Schema;
132
133    impl Schema {
134        pub fn load(path: impl AsRef<Path>) -> Result<Self, DeserializeError> {
135            Self::strict_deserialize_from_file::<{ usize::MAX }>(path)
136        }
137
138        pub fn save(&self, path: impl AsRef<Path>) -> Result<(), SerializeError> {
139            self.strict_serialize_to_file::<{ usize::MAX }>(path)
140        }
141    }
142}