sonicapi/
api.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//! API defines how software can interface a contract.
25//!
26//! SONIC provides four types of actions for working with contract (ROVT):
27//! 1. _Read_ the state of the contract;
28//! 2. _Operate_: construct new operations performing contract state transitions;
29//! 3. _Verify_ an existing operation under the contract Codex and generate transaction;
30//! 4. _Transact_: apply or roll-back transactions to the contract state.
31//!
32//! API defines methods for human-based interaction with the contract for read and operate actions.
33//! The "verify" part is implemented in the consensus layer (UltraSONIC), the "transact" part is
34//! performed directly, so these two are not covered by an API.
35
36use core::cmp::Ordering;
37use core::fmt::Debug;
38use core::hash::{Hash, Hasher};
39use core::num::ParseIntError;
40
41use aluvm::{Lib, LibId};
42use amplify::confinement::{SmallOrdMap, SmallOrdSet, TinyOrdMap, TinyOrdSet, TinyString};
43use amplify::num::u256;
44use amplify::Bytes4;
45use baid64::Baid64ParseError;
46use commit_verify::{CommitEncode, CommitEngine, CommitId, StrictHash};
47use indexmap::{indexset, IndexMap, IndexSet};
48use sonic_callreq::{CallState, MethodName, StateName};
49use strict_encoding::TypeName;
50use strict_types::{SemId, StrictDecode, StrictDumb, StrictEncode, StrictVal, TypeSystem};
51use ultrasonic::{CallId, Codex, CodexId, StateData, StateValue};
52
53use crate::{
54    Aggregator, RawBuilder, RawConvertor, StateArithm, StateAtom, StateBuildError, StateBuilder, StateCalc,
55    StateConvertError, StateConvertor, LIB_NAME_SONIC,
56};
57
58/// Errors happening during parsing of a versioned contract or codex ID.
59#[derive(Debug, Display, Error, From)]
60#[display(doc_comments)]
61pub enum ParseVersionedError {
62    /// the versioned id '{0}' misses the version component, which should be provided after a `/`
63    /// sign.
64    NoVersion(String),
65    /// the versioned id '{0}' misses the API checksum component, which should be provided after a
66    /// `#` sign.
67    NoChecksum(String),
68    /// invalid versioned identifier; {0}
69    Id(Baid64ParseError),
70    #[from]
71    /// invalid versioned number; {0}
72    Version(ParseIntError),
73    /// invalid API checksum value; {0}
74    Checksum(Baid64ParseError),
75}
76
77/// API checksum computed from a set of contract APIs present in [`Semantics`].
78///
79/// # Nota bene
80///
81/// This is not a unique identifier!
82/// It is created just for UI, so users can easily visually distinguish different sets of APIs from
83/// each other.
84///
85/// This type is not - and must not be used in any verification.
86#[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
87#[wrapper(Deref, BorrowSlice, Hex, Index, RangeOps)]
88#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
89#[strict_type(lib = LIB_NAME_SONIC)]
90pub struct ApisChecksum(
91    #[from]
92    #[from([u8; 4])]
93    Bytes4,
94);
95
96mod _baid4 {
97    use core::fmt::{self, Display, Formatter};
98    use core::str::FromStr;
99
100    use amplify::ByteArray;
101    use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str};
102    use commit_verify::{CommitmentId, DigestExt, Sha256};
103
104    use super::*;
105
106    impl DisplayBaid64<4> for ApisChecksum {
107        const HRI: &'static str = "api";
108        const CHUNKING: bool = false;
109        const PREFIX: bool = false;
110        const EMBED_CHECKSUM: bool = false;
111        const MNEMONIC: bool = false;
112        fn to_baid64_payload(&self) -> [u8; 4] { self.to_byte_array() }
113    }
114    impl FromBaid64Str<4> for ApisChecksum {}
115    impl FromStr for ApisChecksum {
116        type Err = Baid64ParseError;
117        fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
118    }
119    impl Display for ApisChecksum {
120        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
121    }
122
123    impl From<Sha256> for ApisChecksum {
124        fn from(hasher: Sha256) -> Self {
125            let hash = hasher.finish();
126            Self::from_slice_checked(&hash[..4])
127        }
128    }
129
130    impl CommitmentId for ApisChecksum {
131        const TAG: &'static str = "urn:ubideco:sonic:apis#2025-05-25";
132    }
133
134    #[cfg(feature = "serde")]
135    ultrasonic::impl_serde_str_bin_wrapper!(ApisChecksum, Bytes4);
136}
137
138/// A helper structure to store the contract semantics, made of a set of APIs, corresponding type
139/// system, and libs, used by the codex.
140///
141/// A contract may have multiple APIs defined; this structure summarizes information about them.
142/// The structure also holds a set of AluVM libraries for the codex and type system used by the
143/// APIs.
144#[derive(Clone, Eq, Debug)]
145#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
146#[strict_type(lib = LIB_NAME_SONIC)]
147#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
148pub struct Semantics {
149    /// Backward-compatible version number for the issuer.
150    ///
151    /// This version number is used to decide which contract APIs to apply if multiple
152    /// contract APIs are available.
153    pub version: u16,
154    /// The default API.
155    pub default: Api,
156    /// The custom named APIs.
157    ///
158    /// The mechanism of the custom APIs allows a contract to have multiple implementations
159    /// of the same interface.
160    ///
161    /// For instance, a contract may provide multiple tokens using different token names.
162    pub custom: SmallOrdMap<TypeName, Api>,
163    /// A set of zk-AluVM libraries called from the contract codex.
164    pub codex_libs: SmallOrdSet<Lib>,
165    /// A set of AluVM libraries called from the APIs.
166    pub api_libs: SmallOrdSet<Lib>,
167    /// The type system used by the contract APIs.
168    pub types: TypeSystem,
169}
170
171impl PartialEq for Semantics {
172    fn eq(&self, other: &Self) -> bool {
173        self.default.codex_id == other.default.codex_id
174            && self.version == other.version
175            && self.commit_id() == other.commit_id()
176    }
177}
178impl PartialOrd for Semantics {
179    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
180}
181impl Ord for Semantics {
182    fn cmp(&self, other: &Self) -> Ordering {
183        match self.default.codex_id.cmp(&other.default.codex_id) {
184            Ordering::Equal => match self.version.cmp(&other.version) {
185                Ordering::Equal => self.commit_id().cmp(&other.commit_id()),
186                other => other,
187            },
188            other => other,
189        }
190    }
191}
192
193impl CommitEncode for Semantics {
194    type CommitmentId = ApisChecksum;
195    fn commit_encode(&self, e: &mut CommitEngine) {
196        e.commit_to_serialized(&self.version);
197        e.commit_to_hash(&self.default);
198        // We do not commit to the codex_libs since they are not a part of APIs
199        // and are commit to inside the codex.
200        // The fact that there are no other libs
201        // is verified in the Articles and Issuer constructors.
202        let apis = SmallOrdMap::from_iter_checked(
203            self.custom
204                .iter()
205                .map(|(name, api)| (name.clone(), api.api_id())),
206        );
207        e.commit_to_linear_map(&apis);
208        let libs = SmallOrdSet::from_iter_checked(self.api_libs.iter().map(Lib::lib_id));
209        e.commit_to_linear_set(&libs);
210        e.commit_to_serialized(&self.types.id());
211    }
212}
213
214impl Semantics {
215    pub fn apis_checksum(&self) -> ApisChecksum { self.commit_id() }
216
217    /// Iterates over all APIs, including default and named ones.
218    pub fn apis(&self) -> impl Iterator<Item = &Api> { [&self.default].into_iter().chain(self.custom.values()) }
219
220    /// Check whether this semantics object matches codex and the provided set of libraries for it.
221    pub fn check(&self, codex: &Codex) -> Result<(), SemanticError> {
222        let codex_id = codex.codex_id();
223
224        let mut ids = bset![];
225        for api in self.apis() {
226            if api.codex_id != codex_id {
227                return Err(SemanticError::CodexMismatch);
228            }
229            let api_id = api.api_id();
230            if !ids.insert(api_id) {
231                return Err(SemanticError::DuplicatedApi(api_id));
232            }
233        }
234
235        // Check codex libs for redundancies and completeness
236        let lib_map = self
237            .codex_libs
238            .iter()
239            .map(|lib| (lib.lib_id(), lib))
240            .collect::<IndexMap<_, _>>();
241
242        let mut lib_ids = codex
243            .verifiers
244            .values()
245            .map(|entry| entry.lib_id)
246            .collect::<IndexSet<_>>();
247        let mut i = 0usize;
248        let mut count = lib_ids.len();
249        while i < count {
250            let id = lib_ids.get_index(i).expect("index is valid");
251            let lib = lib_map.get(id).ok_or(SemanticError::MissedCodexLib(*id))?;
252            lib_ids.extend(lib.libs.iter().copied());
253            count = lib_ids.len();
254            i += 1;
255        }
256        for id in lib_map.keys() {
257            if !lib_ids.contains(id) {
258                return Err(SemanticError::ExcessiveCodexLib(*id));
259            }
260        }
261
262        // Check API libs for redundancies and completeness
263        let lib_map = self
264            .api_libs
265            .iter()
266            .map(|lib| (lib.lib_id(), lib))
267            .collect::<IndexMap<_, _>>();
268
269        let mut lib_ids = indexset![];
270        for api in self.apis() {
271            for agg in api.aggregators.values() {
272                if let Aggregator::AluVM(entry) = agg {
273                    lib_ids.insert(entry.lib_id);
274                }
275            }
276            for glob in api.global.values() {
277                if let StateConvertor::AluVM(entry) = glob.convertor {
278                    lib_ids.insert(entry.lib_id);
279                }
280                if let StateBuilder::AluVM(entry) = glob.builder {
281                    lib_ids.insert(entry.lib_id);
282                }
283                if let RawConvertor::AluVM(entry) = glob.raw_convertor {
284                    lib_ids.insert(entry.lib_id);
285                }
286                if let RawBuilder::AluVM(entry) = glob.raw_builder {
287                    lib_ids.insert(entry.lib_id);
288                }
289            }
290            for owned in api.owned.values() {
291                if let StateConvertor::AluVM(entry) = owned.convertor {
292                    lib_ids.insert(entry.lib_id);
293                }
294                if let StateBuilder::AluVM(entry) = owned.builder {
295                    lib_ids.insert(entry.lib_id);
296                }
297                if let StateBuilder::AluVM(entry) = owned.witness_builder {
298                    lib_ids.insert(entry.lib_id);
299                }
300                if let StateArithm::AluVM(entry) = owned.arithmetics {
301                    lib_ids.insert(entry.lib_id);
302                }
303            }
304        }
305        let mut i = 0usize;
306        let mut count = lib_ids.len();
307        while i < count {
308            let id = lib_ids.get_index(i).expect("index is valid");
309            let lib = lib_map.get(id).ok_or(SemanticError::MissedApiLib(*id))?;
310            lib_ids.extend(lib.libs.iter().copied());
311            count = lib_ids.len();
312            i += 1;
313        }
314        for id in lib_map.keys() {
315            if !lib_ids.contains(id) {
316                return Err(SemanticError::ExcessiveApiLib(*id));
317            }
318        }
319
320        Ok(())
321    }
322}
323
324/// API is an interface implementation.
325///
326/// API should work without requiring runtime to have corresponding interfaces; it should provide
327/// all necessary data. Basically, one may think of API as a compiled interface hierarchy applied to
328/// a specific codex.
329///
330/// API does not commit to an interface, since it can match multiple interfaces in the interface
331/// hierarchy.
332#[derive(Getters, Clone, Debug)]
333#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
334#[strict_type(lib = LIB_NAME_SONIC)]
335#[derive(CommitEncode)]
336#[commit_encode(strategy = strict, id = StrictHash)]
337#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase", bound = ""))]
338pub struct Api {
339    /// Commitment to the codex under which the API is valid.
340    #[getter(as_copy)]
341    pub codex_id: CodexId,
342
343    /// Interface standards to which the API conforms.
344    pub conforms: TinyOrdSet<u16>,
345
346    /// Name for the default API call and owned state name.
347    pub default_call: Option<CallState>,
348
349    /// State API defines how a structured global contract state is constructed out of (and
350    /// converted into) UltraSONIC immutable memory cells.
351    pub global: TinyOrdMap<StateName, GlobalApi>,
352
353    /// State API defines how a structured owned contract state is constructed out of (and converted
354    /// into) UltraSONIC destructible memory cells.
355    pub owned: TinyOrdMap<StateName, OwnedApi>,
356
357    /// Readers have access to the converted global `state` and can construct a derived state out of
358    /// it.
359    ///
360    /// The typical examples when readers are used are to sum individual asset issues and compute
361    /// the number of totally issued assets.
362    pub aggregators: TinyOrdMap<MethodName, Aggregator>,
363
364    /// Links between named transaction methods defined in the interface - and corresponding
365    /// verifier call ids defined by the contract.
366    ///
367    /// NB: Multiple methods from the interface may call the came verifier.
368    pub verifiers: TinyOrdMap<MethodName, CallId>,
369
370    /// Maps error type reported by a contract verifier via `EA` value to an error description taken
371    /// from the interfaces.
372    pub errors: TinyOrdMap<u256, TinyString>,
373}
374
375impl PartialEq for Api {
376    fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal }
377}
378impl Eq for Api {}
379impl PartialOrd for Api {
380    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
381}
382impl Ord for Api {
383    fn cmp(&self, other: &Self) -> Ordering { self.api_id().cmp(&other.api_id()) }
384}
385impl Hash for Api {
386    fn hash<H: Hasher>(&self, state: &mut H) { self.api_id().hash(state); }
387}
388
389impl Api {
390    pub fn api_id(&self) -> StrictHash { self.commit_id() }
391
392    pub fn verifier(&self, method: impl Into<MethodName>) -> Option<CallId> {
393        self.verifiers.get(&method.into()).copied()
394    }
395
396    pub fn convert_global(
397        &self,
398        data: &StateData,
399        sys: &TypeSystem,
400    ) -> Result<Option<(StateName, StateAtom)>, StateConvertError> {
401        // Here we do not yet know which state we are using, since it is encoded inside the field element
402        // of `StateValue`. Thus, we are trying all available convertors until they succeed, since the
403        // convertors check the state type. Then, we use the state name associated with the succeeding
404        // convertor.
405        for (name, api) in &self.global {
406            if let Some(verified) = api.convertor.convert(api.sem_id, data.value, sys)? {
407                let unverified =
408                    if let Some(raw) = data.raw.as_ref() { Some(api.raw_convertor.convert(raw, sys)?) } else { None };
409                return Ok(Some((name.clone(), StateAtom { verified, unverified })));
410            }
411        }
412        // This means this state is unrelated to this API
413        Ok(None)
414    }
415
416    pub fn convert_owned(
417        &self,
418        value: StateValue,
419        sys: &TypeSystem,
420    ) -> Result<Option<(StateName, StrictVal)>, StateConvertError> {
421        // Here we do not yet know which state we are using, since it is encoded inside the field element
422        // of `StateValue`. Thus, we are trying all available convertors until they succeed, since the
423        // convertors check the state type. Then, we use the state name associated with the succeeding
424        // convertor.
425        for (name, api) in &self.owned {
426            if let Some(atom) = api.convertor.convert(api.sem_id, value, sys)? {
427                return Ok(Some((name.clone(), atom)));
428            }
429        }
430        // This means this state is unrelated to this API
431        Ok(None)
432    }
433
434    #[allow(clippy::result_large_err)]
435    pub fn build_immutable(
436        &self,
437        name: impl Into<StateName>,
438        data: StrictVal,
439        raw: Option<StrictVal>,
440        sys: &TypeSystem,
441    ) -> Result<StateData, StateBuildError> {
442        let name = name.into();
443        let api = self
444            .global
445            .get(&name)
446            .ok_or(StateBuildError::UnknownStateName(name))?;
447        let value = api.builder.build(api.sem_id, data, sys)?;
448        let raw = raw.map(|raw| api.raw_builder.build(raw, sys)).transpose()?;
449        Ok(StateData { value, raw })
450    }
451
452    #[allow(clippy::result_large_err)]
453    pub fn build_destructible(
454        &self,
455        name: impl Into<StateName>,
456        data: StrictVal,
457        sys: &TypeSystem,
458    ) -> Result<StateValue, StateBuildError> {
459        let name = name.into();
460        let api = self
461            .owned
462            .get(&name)
463            .ok_or(StateBuildError::UnknownStateName(name))?;
464
465        api.builder.build(api.sem_id, data, sys)
466    }
467
468    #[allow(clippy::result_large_err)]
469    pub fn build_witness(
470        &self,
471        name: impl Into<StateName>,
472        data: StrictVal,
473        sys: &TypeSystem,
474    ) -> Result<StateValue, StateBuildError> {
475        let name = name.into();
476        let api = self
477            .owned
478            .get(&name)
479            .ok_or(StateBuildError::UnknownStateName(name))?;
480
481        api.witness_builder.build(api.witness_sem_id, data, sys)
482    }
483
484    pub fn calculate(&self, name: impl Into<StateName>) -> Result<StateCalc, StateUnknown> {
485        let name = name.into();
486        let api = self.owned.get(&name).ok_or(StateUnknown(name))?;
487
488        Ok(api.arithmetics.calculator())
489    }
490}
491
492/// API for global (immutable, or append-only) state.
493///
494/// API covers two main functions: taking structured data from the user input and _building_ a valid
495/// state included in a new contract operation - and taking contract state and _converting_ it
496/// into a user-friendly form, as a structured data (which may be lately used by _readers_
497/// performing aggregation of state into a collection-type object).
498#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
499#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
500#[strict_type(lib = LIB_NAME_SONIC)]
501#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
502pub struct GlobalApi {
503    /// Semantic type id for verifiable part of the state.
504    pub sem_id: SemId,
505
506    /// Whether the state is a published state.
507    pub published: bool,
508
509    /// Procedure which converts a state made of finite field elements [`StateValue`] into a
510    /// structured type [`StrictVal`].
511    pub convertor: StateConvertor,
512
513    /// Procedure which builds a state in the form of field elements [`StateValue`] out of a
514    /// structured type [`StrictVal`].
515    pub builder: StateBuilder,
516
517    /// Procedure which converts a state made of raw bytes [`RawState`] into a structured type
518    /// [`StrictVal`].
519    pub raw_convertor: RawConvertor,
520
521    /// Procedure which builds a state in the form of raw bytes [`RawState`] out of a structured
522    /// type [`StrictVal`].
523    pub raw_builder: RawBuilder,
524}
525
526/// API for owned (destrictible, or read-once) state.
527///
528/// API covers two main functions: taking structured data from the user input and _building_ a valid
529/// state included in a new contract operation - and taking contract state and _converting_ it
530/// into a user-friendly form, as structured data. It also allows constructing a state for witness,
531/// allowing destroying previously defined state.
532#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
533#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
534#[strict_type(lib = LIB_NAME_SONIC)]
535#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
536pub struct OwnedApi {
537    /// Semantic type id for the structured converted state data.
538    pub sem_id: SemId,
539
540    /// State arithmetics engine used in constructing new contract operations.
541    pub arithmetics: StateArithm,
542
543    /// Procedure which converts a state made of finite field elements [`StateValue`] into a
544    /// structured type [`StrictVal`].
545    pub convertor: StateConvertor,
546
547    /// Procedure which builds a state in the form of field elements [`StateValue`] out of a
548    /// structured type [`StrictVal`].
549    pub builder: StateBuilder,
550
551    /// Semantic type id for the witness data.
552    pub witness_sem_id: SemId,
553
554    /// Procedure which converts structured data in the form of [`StrictVal`] into a witness made of
555    /// finite field elements in the form of [`StateValue`] for the destroyed previous state (an
556    /// input of an operation).
557    pub witness_builder: StateBuilder,
558}
559
560/// Error indicating that an API was asked to convert a state which is not known to it.
561#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
562#[display("unknown state name '{0}'")]
563pub struct StateUnknown(pub StateName);
564
565/// Errors happening if it is attempted to construct an invalid semantic object [`Semantics`] or
566/// upgrade it inside a contract issuer or articles.
567#[derive(Clone, Eq, PartialEq, Debug, Display, Error)]
568#[display(doc_comments)]
569pub enum SemanticError {
570    /// contract id for the merged contract articles doesn't match.
571    ContractMismatch,
572
573    /// codex id for the merged articles doesn't match.
574    CodexMismatch,
575
576    /// articles contain duplicated API {0} under a different name.
577    DuplicatedApi(StrictHash),
578
579    /// library {0} is used by the contract codex verifiers but absent from the articles.
580    MissedCodexLib(LibId),
581
582    /// library {0} is present in the contract articles but not used in the codex verifiers.
583    ExcessiveCodexLib(LibId),
584
585    /// library {0} is used by the contract APIs but absent from the articles.
586    MissedApiLib(LibId),
587
588    /// library {0} is present in the contract articles but not used in the APIs.
589    ExcessiveApiLib(LibId),
590
591    /// invalid signature over the contract articles.
592    InvalidSignature,
593}