sonicapi/
issuer.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 core::fmt;
25use core::fmt::{Display, Formatter};
26use core::str::FromStr;
27
28use aluvm::{Lib, LibId};
29use amplify::confinement::TinyString;
30use baid64::DisplayBaid64;
31use commit_verify::{CommitEncode, CommitId, StrictHash};
32use sonic_callreq::MethodName;
33use strict_encoding::{StrictDecode, StrictDumb, StrictEncode, TypeName};
34use strict_types::TypeSystem;
35use ultrasonic::{CallId, Codex, CodexId, Identity, LibRepo};
36
37use crate::{Api, ApisChecksum, ParseVersionedError, SemanticError, Semantics, SigBlob, LIB_NAME_SONIC};
38
39/// Issuer id is a versioned variant for the codex id, which includes information about a
40/// specific API version.
41///
42/// Codexes may have multiple API implementations, which may be versioned.
43/// Issuers include a specific version of the codex APIs.
44/// This structure provides the necessary information for the user about a specific API version
45/// known and used by a system, so a user may avoid confusion when an API change due to upgrade
46/// happens.
47///
48/// # See also
49///
50/// - [`CodexId`]
51/// - [`crate::ArticlesId`]
52#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
53#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
54#[strict_type(lib = LIB_NAME_SONIC)]
55#[derive(CommitEncode)]
56#[commit_encode(strategy = strict, id = StrictHash)]
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
58pub struct IssuerId {
59    /// An identifier of the codex.
60    pub codex_id: CodexId,
61    /// Version number of the API.
62    pub version: u16,
63    /// A checksum for the APIs from the Semantics structure.
64    pub checksum: ApisChecksum,
65}
66
67impl Display for IssuerId {
68    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
69        write!(f, "{:#}/{}#", self.codex_id, self.version)?;
70        self.checksum.fmt_baid64(f)
71    }
72}
73
74impl FromStr for IssuerId {
75    type Err = ParseVersionedError;
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        let (id, remnant) = s
78            .split_once('/')
79            .ok_or_else(|| ParseVersionedError::NoVersion(s.to_string()))?;
80        let (version, api_id) = remnant
81            .split_once('#')
82            .ok_or_else(|| ParseVersionedError::NoChecksum(s.to_string()))?;
83        Ok(Self {
84            codex_id: id.parse().map_err(ParseVersionedError::Id)?,
85            version: version.parse().map_err(ParseVersionedError::Version)?,
86            checksum: api_id.parse().map_err(ParseVersionedError::Checksum)?,
87        })
88    }
89}
90
91/// An issuer contains information required for the creation of a contract and interaction with an
92/// existing contract.
93///
94/// # Invariance
95///
96/// The structure provides the following invariance guarantees:
97/// - all the API codex matches the codex under which the contract was issued;
98/// - all the API ids are unique;
99/// - all custom APIs have unique names.
100#[derive(Clone, Eq, PartialEq, Debug)]
101#[derive(StrictType, StrictDumb, StrictEncode)]
102// We must not derive or implement StrictDecode for Issuer, since we cannot validate signature
103// inside it
104#[strict_type(lib = LIB_NAME_SONIC)]
105pub struct Issuer {
106    /// Codex data.
107    codex: Codex,
108    /// A dedicated substructure [`Semantics`] keeping shared parts of both [`Issuer`] and
109    /// [`Articles`].
110    semantics: Semantics,
111    /// Signature of a developer (`codex.developer`) over the [`IssuerId`] for a standalone issuer;
112    /// and from a contract issuer (`issue.meta.issuer`) for an issuer instance within a contract.
113    sig: Option<SigBlob>,
114}
115
116impl Issuer {
117    /// Construct issuer from a codex and its semantics.
118    pub fn new(codex: Codex, semantics: Semantics) -> Result<Self, SemanticError> {
119        semantics.check(&codex)?;
120        Ok(Self { semantics, codex, sig: None })
121    }
122
123    /// Construct issuer from a codex and signed semantics.
124    pub fn with<E>(
125        codex: Codex,
126        semantics: Semantics,
127        sig: SigBlob,
128        sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
129    ) -> Result<Self, SemanticError> {
130        let mut me = Self::new(codex, semantics)?;
131        let id = me.issuer_id().commit_id();
132        sig_validator(id, &me.codex.developer, &sig).map_err(|_| SemanticError::InvalidSignature)?;
133        me.sig = Some(sig);
134        Ok(me)
135    }
136
137    pub fn dismember(self) -> (Codex, Semantics) { (self.codex, self.semantics) }
138
139    /// Compute an issuer id, which includes information about the codex id, API version and
140    /// checksum.
141    pub fn issuer_id(&self) -> IssuerId {
142        IssuerId {
143            codex_id: self.codex.codex_id(),
144            version: self.semantics.version,
145            checksum: self.semantics.apis_checksum(),
146        }
147    }
148    /// Compute a codex id.
149    pub fn codex_id(&self) -> CodexId { self.codex.codex_id() }
150    /// Get a reference to the underlying codex.
151    pub fn codex(&self) -> &Codex { &self.codex }
152    /// Get the name of the underlying codex.
153    pub fn codex_name(&self) -> &TinyString { &self.codex.name }
154
155    /// Get a reference to the contract semantic.
156    pub fn semantics(&self) -> &Semantics { &self.semantics }
157    /// Get a reference to the default API.
158    pub fn default_api(&self) -> &Api { &self.semantics.default }
159    /// Get an iterator over the custom APIs.
160    pub fn custom_apis(&self) -> impl Iterator<Item = (&TypeName, &Api)> { self.semantics.custom.iter() }
161    /// Get a reference to the type system.
162    pub fn types(&self) -> &TypeSystem { &self.semantics.types }
163    /// Iterates over all APIs, including the default and the named ones.
164    pub fn apis(&self) -> impl Iterator<Item = &Api> { self.semantics.apis() }
165    /// Iterates over all codex libraries.
166    pub fn codex_libs(&self) -> impl Iterator<Item = &Lib> { self.semantics.codex_libs.iter() }
167
168    /// Detect whether the issuer is signed.
169    pub fn is_signed(&self) -> bool { self.sig.is_some() }
170
171    /// Get a [`CallId`] for a method from the default API.
172    ///
173    /// # Panics
174    ///
175    /// If the method name is not known.
176    pub fn call_id(&self, method: impl Into<MethodName>) -> CallId {
177        self.semantics
178            .default
179            .verifier(method)
180            .expect("calling to method absent in Codex API")
181    }
182}
183
184impl LibRepo for Issuer {
185    fn get_lib(&self, lib_id: LibId) -> Option<&Lib> {
186        self.semantics
187            .codex_libs
188            .iter()
189            .find(|lib| lib.lib_id() == lib_id)
190    }
191}
192
193#[cfg(feature = "binfile")]
194mod _fs {
195    use std::io::{self, Read};
196    use std::path::Path;
197
198    use amplify::confinement::U24 as U24MAX;
199    use binfile::BinFile;
200    use commit_verify::{CommitId, StrictHash};
201    use strict_encoding::{DecodeError, DeserializeError, StreamReader, StreamWriter, StrictDecode, StrictEncode};
202    use ultrasonic::{Codex, Identity};
203
204    use crate::{Issuer, Semantics, SigBlob};
205
206    /// The magic number used in storing issuer as a binary file.
207    pub const ISSUER_MAGIC_NUMBER: u64 = u64::from_be_bytes(*b"ISSUER  ");
208    /// The issuer encoding version used in storing issuer as a binary file.
209    pub const ISSUER_VERSION: u16 = 0;
210
211    impl Issuer {
212        pub fn load<E>(
213            path: impl AsRef<Path>,
214            sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
215        ) -> Result<Self, DeserializeError> {
216            // We use a manual implementation since we can't validate signature inside StrictDecode
217            // implementation for the Issuer
218            let file = BinFile::<ISSUER_MAGIC_NUMBER, ISSUER_VERSION>::open(path)?;
219            let mut reader = StreamReader::new::<U24MAX>(file);
220
221            let codex = Codex::strict_read(&mut reader)?;
222            let semantics = Semantics::strict_read(&mut reader)?;
223            semantics
224                .check(&codex)
225                .map_err(|e| DecodeError::DataIntegrityError(e.to_string()))?;
226
227            let sig = Option::<SigBlob>::strict_read(&mut reader)?;
228            let me = Self { codex, semantics, sig };
229
230            if let Some(sig) = &me.sig {
231                sig_validator(me.issuer_id().commit_id(), &me.codex.developer, sig)
232                    .map_err(|_| DecodeError::DataIntegrityError(s!("invalid signature")))?;
233            }
234
235            match reader.unconfine().read_exact(&mut [0u8; 1]) {
236                Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(me),
237                Err(e) => Err(e.into()),
238                Ok(()) => Err(DeserializeError::DataNotEntirelyConsumed),
239            }
240        }
241
242        pub fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
243            let file = BinFile::<ISSUER_MAGIC_NUMBER, ISSUER_VERSION>::create_new(path)?;
244            let writer = StreamWriter::new::<U24MAX>(file);
245            self.strict_write(writer)
246        }
247    }
248}
249#[cfg(feature = "binfile")]
250pub use _fs::*;