sonicapi/
articles.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
24#![allow(unused_braces)]
25
26use core::fmt;
27use core::fmt::{Display, Formatter};
28use core::str::FromStr;
29
30use aluvm::{Lib, LibId};
31use amplify::confinement::NonEmptyBlob;
32use amplify::Wrapper;
33use baid64::DisplayBaid64;
34use commit_verify::{CommitEncode, CommitId, StrictHash};
35use sonic_callreq::MethodName;
36use strict_encoding::TypeName;
37use strict_types::TypeSystem;
38use ultrasonic::{
39    CallId, Codex, CodexId, ContractId, ContractMeta, ContractName, Genesis, Identity, Issue, LibRepo, Opid,
40};
41
42use crate::{Api, ApisChecksum, ParseVersionedError, SemanticError, Semantics, LIB_NAME_SONIC};
43
44/// Articles id is a versioned variant for the contract id, which includes information about a
45/// specific API version.
46///
47/// Contracts may have multiple API implementations, which may be versioned.
48/// Articles include a specific version of the contract APIs.
49/// This structure provides the necessary information for the user about a specific API version
50/// known and used by a system, so a user may avoid confusion when an API change due to upgrade
51/// happens.
52///
53/// # See also
54///
55/// - [`ContractId`]
56/// - [`crate::IssuerId`]
57#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
58#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
59#[strict_type(lib = LIB_NAME_SONIC)]
60#[derive(CommitEncode)]
61#[commit_encode(strategy = strict, id = StrictHash)]
62#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
63pub struct ArticlesId {
64    /// An identifier of the contract.
65    pub contract_id: ContractId,
66    /// Version number of the API.
67    pub version: u16,
68    /// A checksum for the APIs from the Semantics structure.
69    pub checksum: ApisChecksum,
70}
71
72impl Display for ArticlesId {
73    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
74        write!(f, "{}/{}#", self.contract_id, self.version)?;
75        self.checksum.fmt_baid64(f)
76    }
77}
78
79impl FromStr for ArticlesId {
80    type Err = ParseVersionedError;
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        let (id, remnant) = s
83            .split_once('/')
84            .ok_or_else(|| ParseVersionedError::NoVersion(s.to_string()))?;
85        let (version, api_id) = remnant
86            .split_once('#')
87            .ok_or_else(|| ParseVersionedError::NoChecksum(s.to_string()))?;
88        Ok(Self {
89            contract_id: id.parse().map_err(ParseVersionedError::Id)?,
90            version: version.parse().map_err(ParseVersionedError::Version)?,
91            checksum: api_id.parse().map_err(ParseVersionedError::Checksum)?,
92        })
93    }
94}
95
96/// Articles contain the contract and all related codex and API information for interacting with it.
97///
98/// # Invariance
99///
100/// The structure provides the following invariance guarantees:
101/// - all the API codex matches the codex under which the contract was issued;
102/// - all the API ids are unique;
103/// - all custom APIs have unique names;
104/// - the signature, if present, is a valid sig over the [`ArticlesId`].
105#[derive(Clone, Eq, PartialEq, Debug)]
106#[derive(StrictType, StrictDumb, StrictEncode)]
107// We must not derive or implement StrictDecode for Issuer, since we cannot validate signature
108// inside it
109#[strict_type(lib = LIB_NAME_SONIC)]
110pub struct Articles {
111    /// We can't use [`Issuer`] here since we will duplicate the codex between it and the [`Issue`].
112    /// Thus, a dedicated substructure [`Semantics`] is introduced, which keeps a shared part of
113    /// both [`Issuer`] and [`Articles`].
114    semantics: Semantics,
115    /// Signature from the contract issuer (`issue.meta.issuer`) over the articles' id.
116    ///
117    /// NB: it must precede the issue, which contains genesis!
118    /// Since genesis is read with a stream-supporting procedure later.
119    sig: Option<SigBlob>,
120    /// The contract issue.
121    issue: Issue,
122}
123
124impl Articles {
125    /// Construct articles from a signed contract semantic and the contract issue under that
126    /// semantics.
127    pub fn with<E>(
128        semantics: Semantics,
129        issue: Issue,
130        sig: Option<SigBlob>,
131        sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
132    ) -> Result<Self, SemanticError> {
133        semantics.check(&issue.codex)?;
134        let mut me = Self { semantics, issue, sig: None };
135        let id = me.articles_id().commit_id();
136        if let Some(sig) = &sig {
137            sig_validator(id, &me.issue.meta.issuer, sig).map_err(|_| SemanticError::InvalidSignature)?;
138        }
139        me.sig = sig;
140        Ok(me)
141    }
142
143    /// Compute an article id, which includes information about the contract id, API version and
144    /// checksum.
145    pub fn articles_id(&self) -> ArticlesId {
146        ArticlesId {
147            contract_id: self.issue.contract_id(),
148            version: self.semantics.version,
149            checksum: self.semantics.apis_checksum(),
150        }
151    }
152    /// Compute a contract id.
153    pub fn contract_id(&self) -> ContractId { self.issue.contract_id() }
154    /// Compute a codex id.
155    pub fn codex_id(&self) -> CodexId { self.issue.codex_id() }
156    /// Compute a genesis opid.
157    pub fn genesis_opid(&self) -> Opid { self.issue.genesis_opid() }
158
159    /// Get a reference to the contract semantic.
160    pub fn semantics(&self) -> &Semantics { &self.semantics }
161    /// Get a reference to the default API.
162    pub fn default_api(&self) -> &Api { &self.semantics.default }
163    /// Get an iterator over the custom APIs.
164    pub fn custom_apis(&self) -> impl Iterator<Item = (&TypeName, &Api)> { self.semantics.custom.iter() }
165    /// Get a reference to the type system.
166    pub fn types(&self) -> &TypeSystem { &self.semantics.types }
167    /// Iterates over all APIs, including the default and the named ones.
168    pub fn apis(&self) -> impl Iterator<Item = &Api> { self.semantics.apis() }
169    /// Iterates over all codex libraries.
170    pub fn codex_libs(&self) -> impl Iterator<Item = &Lib> { self.semantics.codex_libs.iter() }
171
172    /// Get a reference to the contract issue information.
173    pub fn issue(&self) -> &Issue { &self.issue }
174    /// Get a reference to the contract codex.
175    pub fn codex(&self) -> &Codex { &self.issue.codex }
176    /// Get a reference to the contract genesis.
177    pub fn genesis(&self) -> &Genesis { &self.issue.genesis }
178    /// Get a reference to the contract meta-information.
179    pub fn contract_meta(&self) -> &ContractMeta { &self.issue.meta }
180    /// Get a reference to the contract name.
181    pub fn contract_name(&self) -> &ContractName { &self.issue.meta.name }
182
183    /// Get a reference to a signature over the contract semantics.
184    pub fn sig(&self) -> &Option<SigBlob> { &self.sig }
185    /// Detect whether the articles are signed.
186    pub fn is_signed(&self) -> bool { self.sig.is_some() }
187
188    /// Upgrades contract APIs if a newer version is available.
189    ///
190    /// # Returns
191    ///
192    /// Whether the upgrade has happened, i.e. `other` represents a valid later version of the APIs.
193    pub fn upgrade_apis(&mut self, other: Self) -> Result<bool, SemanticError> {
194        if self.contract_id() != other.contract_id() {
195            return Err(SemanticError::ContractMismatch);
196        }
197
198        Ok(match (&self.sig, &other.sig) {
199            (None, None) | (Some(_), Some(_)) if other.semantics.version > self.semantics.version => {
200                self.semantics = other.semantics;
201                true
202            }
203            (None, Some(_)) => {
204                self.semantics = other.semantics;
205                true
206            }
207            _ => false, // No upgrade
208        })
209    }
210
211    /// Get a [`CallId`] for a method from the default API.
212    ///
213    /// # Panics
214    ///
215    /// If the method name is not known.
216    pub fn call_id(&self, method: impl Into<MethodName>) -> CallId {
217        let method = method.into();
218        let name = method.to_string();
219        self.semantics
220            .default
221            .verifier(method)
222            .unwrap_or_else(|| panic!("requesting a method `{name}` absent in the contract API"))
223    }
224}
225
226impl LibRepo for Articles {
227    fn get_lib(&self, lib_id: LibId) -> Option<&Lib> {
228        self.semantics
229            .codex_libs
230            .iter()
231            .find(|lib| lib.lib_id() == lib_id)
232    }
233}
234
235/// A signature blob.
236///
237/// Helps to abstract from a specific signing algorithm.
238#[derive(Wrapper, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, From, Display)]
239#[wrapper(Deref, AsSlice, BorrowSlice, Hex)]
240#[display(LowerHex)]
241#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
242#[strict_type(lib = LIB_NAME_SONIC, dumb = { Self(NonEmptyBlob::with(0)) })]
243#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
244pub struct SigBlob(NonEmptyBlob<4096>);
245
246impl SigBlob {
247    /// Constrict sig blob from byte slice.
248    ///
249    /// # Panics
250    ///
251    /// If the slice length is zero or larger than 4096.
252    pub fn from_slice_checked(data: impl AsRef<[u8]>) -> SigBlob {
253        Self(NonEmptyBlob::from_checked(data.as_ref().to_vec()))
254    }
255
256    /// Constrict sig blob from byte vector.
257    ///
258    /// # Panics
259    ///
260    /// If the slice length is zero or larger than 4096.
261    pub fn from_vec_checked(data: Vec<u8>) -> SigBlob { Self(NonEmptyBlob::from_checked(data)) }
262}