sonicapi/
schema.rs

1// SONIC: Standard library 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 std::io;
25
26use aluvm::{Lib, LibId};
27use amplify::confinement::{SmallOrdMap, SmallOrdSet, TinyOrdMap};
28use amplify::hex::ToHex;
29use commit_verify::ReservedBytes;
30use strict_encoding::{
31    DecodeError, ReadRaw, StrictDecode, StrictEncode, StrictReader, StrictWriter, TypeName, WriteRaw,
32};
33use strict_types::TypeSystem;
34use ultrasonic::{CallId, Codex, LibRepo};
35
36use crate::sigs::ContentSigs;
37use crate::{Annotations, Api, MergeError, MethodName, LIB_NAME_SONIC};
38
39pub const SCHEMA_MAGIC_NUMBER: [u8; 8] = *b"COISSUER";
40pub const SCHEMA_VERSION: [u8; 2] = [0x00, 0x01];
41
42/// A schema contains information required for the creation of a contract.
43#[derive(Clone, Eq, PartialEq, Debug)]
44#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
45#[strict_type(lib = LIB_NAME_SONIC)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
47pub struct Schema {
48    pub codex: Codex,
49    pub default_api: Api,
50    pub default_api_sigs: ContentSigs,
51    pub custom_apis: SmallOrdMap<Api, ContentSigs>,
52    pub libs: SmallOrdSet<Lib>,
53    pub types: TypeSystem,
54    pub codex_sigs: ContentSigs,
55    pub annotations: TinyOrdMap<Annotations, ContentSigs>,
56    pub reserved: ReservedBytes<8>,
57}
58
59impl LibRepo for Schema {
60    fn get_lib(&self, lib_id: LibId) -> Option<&Lib> { self.libs.iter().find(|lib| lib.lib_id() == lib_id) }
61}
62
63impl Schema {
64    pub fn new(codex: Codex, api: Api, libs: impl IntoIterator<Item = Lib>, types: TypeSystem) -> Self {
65        // TODO: Ensure default API is unnamed?
66        Schema {
67            codex,
68            default_api: api,
69            default_api_sigs: none!(),
70            custom_apis: none!(),
71            libs: SmallOrdSet::from_iter_checked(libs),
72            types,
73            codex_sigs: none!(),
74            annotations: none!(),
75            reserved: zero!(),
76        }
77    }
78
79    pub fn api(&self, name: &TypeName) -> &Api {
80        self.custom_apis
81            .keys()
82            .find(|api| api.name() == Some(name))
83            .expect("API is not known")
84    }
85
86    pub fn call_id(&self, method: impl Into<MethodName>) -> CallId {
87        self.default_api
88            .verifier(method)
89            .expect("calling to method absent in Codex API")
90    }
91
92    pub fn merge(&mut self, other: Self) -> Result<bool, MergeError> {
93        if self.codex.codex_id() != other.codex.codex_id() {
94            return Err(MergeError::CodexMismatch);
95        }
96        self.codex_sigs.merge(other.codex_sigs);
97
98        if self.default_api != other.default_api {
99            let _ = self
100                .custom_apis
101                .insert(other.default_api, other.default_api_sigs);
102        } else {
103            self.default_api_sigs.merge(other.default_api_sigs);
104        }
105
106        for (api, other_sigs) in other.custom_apis {
107            let Ok(entry) = self.custom_apis.entry(api) else {
108                continue;
109            };
110            entry.or_default().merge(other_sigs);
111        }
112
113        // NB: We must not fail here, since otherwise it opens an attack vector on invalidating valid
114        // consignments by adding too many libs
115        // TODO: Return warnings instead
116        let _ = self.libs.extend(other.libs);
117        let _ = self.types.extend(other.types);
118
119        for (annotation, other_sigs) in other.annotations {
120            let Ok(entry) = self.annotations.entry(annotation) else {
121                continue;
122            };
123            entry.or_default().merge(other_sigs);
124        }
125
126        Ok(true)
127    }
128
129    pub fn decode(reader: &mut StrictReader<impl ReadRaw>) -> Result<Self, DecodeError> {
130        let magic_bytes = <[u8; 8]>::strict_decode(reader)?;
131        if magic_bytes != SCHEMA_MAGIC_NUMBER {
132            return Err(DecodeError::DataIntegrityError(format!(
133                "wrong contract issuer schema magic bytes {}",
134                magic_bytes.to_hex()
135            )));
136        }
137        let version = <[u8; 2]>::strict_decode(reader)?;
138        if version != SCHEMA_VERSION {
139            return Err(DecodeError::DataIntegrityError(format!(
140                "unsupported contract issuer schema version {}",
141                u16::from_be_bytes(version)
142            )));
143        }
144        Self::strict_decode(reader)
145    }
146
147    pub fn encode(&self, mut writer: StrictWriter<impl WriteRaw>) -> io::Result<()> {
148        // This is compatible with BinFile
149        writer = SCHEMA_MAGIC_NUMBER.strict_encode(writer)?;
150        // Version
151        writer = SCHEMA_VERSION.strict_encode(writer)?;
152        self.strict_encode(writer)?;
153        Ok(())
154    }
155}
156
157#[cfg(feature = "std")]
158mod _fs {
159    use std::fs::File;
160    use std::io::{self, Read};
161    use std::path::Path;
162
163    use amplify::confinement::U24 as U24MAX;
164    use strict_encoding::{DeserializeError, StreamReader, StreamWriter, StrictReader, StrictWriter};
165
166    use super::Schema;
167
168    impl Schema {
169        pub fn load(path: impl AsRef<Path>) -> Result<Self, DeserializeError> {
170            let file = File::open(path)?;
171            let mut reader = StrictReader::with(StreamReader::new::<U24MAX>(file));
172            let me = Self::decode(&mut reader)?;
173            match reader.unbox().unconfine().read_exact(&mut [0u8; 1]) {
174                Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(me),
175                Err(e) => Err(e.into()),
176                Ok(()) => Err(DeserializeError::DataNotEntirelyConsumed),
177            }
178        }
179
180        pub fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
181            let file = File::create_new(path)?;
182            let writer = StrictWriter::with(StreamWriter::new::<U24MAX>(file));
183            self.encode(writer)
184        }
185    }
186}