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 a contract can be interfaced by software.
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};
39
40use amplify::confinement::{ConfinedBlob, TinyOrdMap, TinyString, U16 as U16MAX};
41use amplify::num::u256;
42use amplify::Bytes32;
43use commit_verify::{CommitId, ReservedBytes};
44use sonic_callreq::{CallState, MethodName, StateName};
45use strict_types::{SemId, StrictDecode, StrictDumb, StrictEncode, StrictVal, TypeName, TypeSystem};
46use ultrasonic::{CallId, CodexId, Identity, StateData, StateValue};
47
48use crate::embedded::EmbeddedProc;
49use crate::{StateAtom, VmType, LIB_NAME_SONIC};
50
51pub(super) const USED_FIEL_BYTES: usize = u256::BYTES as usize - 2;
52pub(super) const TOTAL_BYTES: usize = USED_FIEL_BYTES * 3;
53
54/// API is an interface implementation.
55///
56/// API should work without requiring runtime to have corresponding interfaces; it should provide
57/// all necessary data. Basically, one may think of API as a compiled interface hierarchy applied to
58/// a specific codex.
59///
60/// API doesn't commit to an interface ID, since it can match multiple interfaces in the interface
61/// hierarchy.
62#[derive(Clone, Debug, From)]
63#[derive(CommitEncode)]
64#[commit_encode(strategy = strict, id = ApiId)]
65#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
66#[strict_type(lib = LIB_NAME_SONIC, tags = custom, dumb = Self::Embedded(strict_dumb!()))]
67#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
68pub enum Api {
69    #[from]
70    #[strict_type(tag = 1)]
71    Embedded(ApiInner<EmbeddedProc>),
72
73    #[from]
74    #[strict_type(tag = 2)]
75    Alu(ApiInner<aluvm::Vm>),
76}
77
78impl PartialEq for Api {
79    fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal }
80}
81impl Eq for Api {}
82impl PartialOrd for Api {
83    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
84}
85impl Ord for Api {
86    fn cmp(&self, other: &Self) -> Ordering {
87        if self.api_id() == other.api_id() {
88            Ordering::Equal
89        } else {
90            self.timestamp().cmp(&other.timestamp())
91        }
92    }
93}
94impl Hash for Api {
95    fn hash<H: Hasher>(&self, state: &mut H) { self.api_id().hash(state); }
96}
97
98impl Api {
99    pub fn api_id(&self) -> ApiId { self.commit_id() }
100
101    pub fn vm_type(&self) -> VmType {
102        match self {
103            Api::Embedded(_) => VmType::Embedded,
104            Api::Alu(_) => VmType::AluVM,
105        }
106    }
107
108    pub fn codex_id(&self) -> CodexId {
109        match self {
110            Api::Embedded(api) => api.codex_id,
111            Api::Alu(api) => api.codex_id,
112        }
113    }
114
115    pub fn timestamp(&self) -> i64 {
116        match self {
117            Api::Embedded(api) => api.timestamp,
118            Api::Alu(api) => api.timestamp,
119        }
120    }
121
122    pub fn name(&self) -> Option<&TypeName> {
123        match self {
124            Api::Embedded(api) => api.name.as_ref(),
125            Api::Alu(api) => api.name.as_ref(),
126        }
127    }
128
129    pub fn conforms(&self) -> Option<&TypeName> {
130        match self {
131            Api::Embedded(api) => api.conforms.as_ref(),
132            Api::Alu(api) => api.conforms.as_ref(),
133        }
134    }
135
136    pub fn developer(&self) -> &Identity {
137        match self {
138            Api::Embedded(api) => &api.developer,
139            Api::Alu(api) => &api.developer,
140        }
141    }
142
143    pub fn default_call(&self) -> Option<&CallState> {
144        match self {
145            Api::Embedded(api) => api.default_call.as_ref(),
146            Api::Alu(api) => api.default_call.as_ref(),
147        }
148    }
149
150    pub fn verifier(&self, method: impl Into<MethodName>) -> Option<CallId> {
151        let method = method.into();
152        match self {
153            Api::Embedded(api) => api.verifiers.get(&method),
154            Api::Alu(api) => api.verifiers.get(&method),
155        }
156        .copied()
157    }
158
159    pub fn readers(&self) -> Box<dyn Iterator<Item = &MethodName> + '_> {
160        match self {
161            Api::Embedded(api) => Box::new(api.readers.keys()),
162            Api::Alu(api) => Box::new(api.readers.keys()),
163        }
164    }
165
166    pub fn read<'s, I: IntoIterator<Item = &'s StateAtom>>(
167        &self,
168        name: &StateName,
169        state: impl Fn(&StateName) -> I,
170    ) -> StrictVal {
171        match self {
172            Api::Embedded(api) => api
173                .readers
174                .get(name)
175                .expect("state name is unknown for the API")
176                .read(state),
177            Api::Alu(api) => api
178                .readers
179                .get(name)
180                .expect("state name is unknown for the API")
181                .read(state),
182        }
183    }
184
185    pub fn convert_immutable(&self, data: &StateData, sys: &TypeSystem) -> Option<(StateName, StateAtom)> {
186        match self {
187            Api::Embedded(api) => {
188                for (name, adaptor) in &api.append_only {
189                    if let Some(atom) = adaptor.convert(data, sys) {
190                        return Some((name.clone(), atom));
191                    }
192                }
193                None
194            }
195            Api::Alu(api) => {
196                for (name, adaptor) in &api.append_only {
197                    if let Some(atom) = adaptor.convert(data, sys) {
198                        return Some((name.clone(), atom));
199                    }
200                }
201                None
202            }
203        }
204    }
205
206    pub fn convert_destructible(&self, value: StateValue, sys: &TypeSystem) -> Option<(StateName, StrictVal)> {
207        // Here we do not yet known which state we are using, since it is encoded inside the field element
208        // of `StateValue`. Thus, we are trying all available convertors until they succeed, since the
209        // convertors check the state type. Then, we use the state name associated with the succeeded
210        // convertor.
211        match self {
212            Api::Embedded(api) => {
213                for (name, adaptor) in &api.destructible {
214                    if let Some(atom) = adaptor.convert(value, sys) {
215                        return Some((name.clone(), atom));
216                    }
217                }
218                None
219            }
220            Api::Alu(api) => {
221                for (name, adaptor) in &api.destructible {
222                    if let Some(atom) = adaptor.convert(value, sys) {
223                        return Some((name.clone(), atom));
224                    }
225                }
226                None
227            }
228        }
229    }
230
231    pub fn build_immutable(
232        &self,
233        name: impl Into<StateName>,
234        data: StrictVal,
235        raw: Option<StrictVal>,
236        sys: &TypeSystem,
237    ) -> StateData {
238        let name = name.into();
239        match self {
240            Api::Embedded(api) => api
241                .append_only
242                .get(&name)
243                .expect("state name is unknown for the API")
244                .build(data, raw, sys),
245            Api::Alu(api) => api
246                .append_only
247                .get(&name)
248                .expect("state name is unknown for the API")
249                .build(data, raw, sys),
250        }
251    }
252
253    pub fn build_destructible(&self, name: impl Into<StateName>, data: StrictVal, sys: &TypeSystem) -> StateValue {
254        let name = name.into();
255        match self {
256            Api::Embedded(api) => api
257                .destructible
258                .get(&name)
259                .expect("state name is unknown for the API")
260                .build(data, sys),
261            Api::Alu(api) => api
262                .destructible
263                .get(&name)
264                .expect("state name is unknown for the API")
265                .build(data, sys),
266        }
267    }
268
269    pub fn calculate(&self, name: impl Into<StateName>) -> Box<dyn StateCalc> {
270        let name = name.into();
271        match self {
272            Api::Embedded(api) => api
273                .destructible
274                .get(&name)
275                .expect("state name is unknown for the API")
276                .arithmetics
277                .calculator(),
278            #[allow(clippy::let_unit_value)]
279            Api::Alu(api) => api
280                .destructible
281                .get(&name)
282                .expect("state name is unknown for the API")
283                .arithmetics
284                .calculator(),
285        }
286    }
287}
288
289#[derive(Clone, Debug)]
290#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
291#[strict_type(lib = LIB_NAME_SONIC)]
292#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase", bound = ""))]
293pub struct ApiInner<Vm: ApiVm> {
294    /// Version of the API structure.
295    pub version: ReservedBytes<2>,
296
297    /// Commitment to the codex under which the API is valid.
298    pub codex_id: CodexId,
299
300    /// Timestamp, which is used for versioning (later APIs have priority over new ones).
301    pub timestamp: i64,
302
303    /// API name. Each codex must have a default API with no name.
304    pub name: Option<TypeName>,
305
306    /// Developer identity string.
307    pub developer: Identity,
308
309    /// Interface standard to which the API conforms.
310    pub conforms: Option<TypeName>,
311
312    /// Name for the default API call and destructible state name.
313    pub default_call: Option<CallState>,
314
315    /// Reserved for future use.
316    pub reserved: ReservedBytes<8>,
317
318    /// State API defines how a structured contract state is constructed out of (and converted into)
319    /// UltraSONIC immutable memory cells.
320    pub append_only: TinyOrdMap<StateName, AppendApi<Vm>>,
321
322    /// State API defines how a structured contract state is constructed out of (and converted into)
323    /// UltraSONIC destructible memory cells.
324    pub destructible: TinyOrdMap<StateName, DestructibleApi<Vm>>,
325
326    /// Readers have access to the converted global `state` and can construct a derived state out of
327    /// it.
328    ///
329    /// The typical examples when readers are used are to sum individual asset issues and compute
330    /// the number of totally issued assets.
331    pub readers: TinyOrdMap<MethodName, Vm::Reader>,
332
333    /// Links between named transaction methods defined in the interface - and corresponding
334    /// verifier call ids defined by the contract.
335    ///
336    /// NB: Multiple methods from the interface may call the came verifier.
337    pub verifiers: TinyOrdMap<MethodName, CallId>,
338
339    /// Maps error type reported by a contract verifier via `EA` value to an error description taken
340    /// from the interfaces.
341    pub errors: TinyOrdMap<u256, TinyString>,
342}
343
344#[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
345#[wrapper(Deref, BorrowSlice, Hex, Index, RangeOps)]
346#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
347#[strict_type(lib = LIB_NAME_SONIC)]
348pub struct ApiId(
349    #[from]
350    #[from([u8; 32])]
351    Bytes32,
352);
353
354mod _baid4 {
355    use core::fmt::{self, Display, Formatter};
356    use core::str::FromStr;
357
358    use amplify::ByteArray;
359    use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str};
360    use commit_verify::{CommitmentId, DigestExt, Sha256};
361
362    use super::*;
363
364    impl DisplayBaid64 for ApiId {
365        const HRI: &'static str = "api";
366        const CHUNKING: bool = true;
367        const PREFIX: bool = false;
368        const EMBED_CHECKSUM: bool = false;
369        const MNEMONIC: bool = true;
370        fn to_baid64_payload(&self) -> [u8; 32] { self.to_byte_array() }
371    }
372    impl FromBaid64Str for ApiId {}
373    impl FromStr for ApiId {
374        type Err = Baid64ParseError;
375        fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
376    }
377    impl Display for ApiId {
378        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
379    }
380
381    impl From<Sha256> for ApiId {
382        fn from(hasher: Sha256) -> Self { hasher.finish().into() }
383    }
384
385    impl CommitmentId for ApiId {
386        const TAG: &'static str = "urn:ubideco:sonic:api#2024-11-20";
387    }
388}
389
390/// API for append-only state.
391///
392/// API covers two main functions: taking structured data from the user input and _building_ a valid
393/// state included into a new contract operation - and taking contract state and _converting_ it
394/// into a user-friendly form, as a structured data (which may be lately used by _readers_
395/// performing aggregation of state into a collection-type objects).
396#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
397#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
398#[strict_type(lib = LIB_NAME_SONIC)]
399#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
400pub struct AppendApi<Vm: ApiVm> {
401    /// Semantic type id for verifiable part of the state.
402    pub sem_id: SemId,
403    /// Semantic type id for non-verifiable part of the state.
404    pub raw_sem_id: SemId,
405
406    pub published: bool,
407    /// Procedures which convert a state made of finite field elements [`StateData`] into a
408    /// structured type [`StructData`] and vice verse.
409    pub adaptor: Vm::Adaptor,
410}
411
412impl<Vm: ApiVm> AppendApi<Vm> {
413    pub fn convert(&self, data: &StateData, sys: &TypeSystem) -> Option<StateAtom> {
414        self.adaptor
415            .convert_immutable(self.sem_id, self.raw_sem_id, data, sys)
416    }
417
418    /// Build an immutable memory cell out of structured state.
419    ///
420    /// Since append-only state includes both field elements (verifiable part of the state) and
421    /// optional structured data (non-verifiable, non-compressible part of the state) it takes
422    /// two inputs of a structured state data, leaving the raw part unchanged.
423    pub fn build(&self, value: StrictVal, raw: Option<StrictVal>, sys: &TypeSystem) -> StateData {
424        let raw = raw.map(|raw| {
425            let typed = sys
426                .typify(raw, self.raw_sem_id)
427                .expect("invalid strict value not matching semantic type information");
428            sys.strict_serialize_value::<U16MAX>(&typed)
429                .expect("strict value is too large")
430                .into()
431        });
432        let value = self.adaptor.build_state(self.sem_id, value, sys);
433        StateData { value, raw }
434    }
435}
436
437#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
438#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
439#[strict_type(lib = LIB_NAME_SONIC)]
440#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
441pub struct DestructibleApi<Vm: ApiVm> {
442    pub sem_id: SemId,
443
444    /// State arithmetics engine used in constructing new contract operations.
445    pub arithmetics: Vm::Arithm,
446
447    /// Procedures which convert a state made of finite field elements [`StateData`] into a
448    /// structured type [`StructData`] and vice verse.
449    pub adaptor: Vm::Adaptor,
450}
451
452impl<Vm: ApiVm> DestructibleApi<Vm> {
453    pub fn convert(&self, value: StateValue, sys: &TypeSystem) -> Option<StrictVal> {
454        self.adaptor.convert_destructible(self.sem_id, value, sys)
455    }
456    pub fn build(&self, value: StrictVal, sys: &TypeSystem) -> StateValue {
457        self.adaptor.build_state(self.sem_id, value, sys)
458    }
459    pub fn arithmetics(&self) -> &Vm::Arithm { &self.arithmetics }
460}
461
462#[cfg(not(feature = "serde"))]
463trait Serde {}
464#[cfg(not(feature = "serde"))]
465impl<T> Serde for T {}
466
467#[cfg(feature = "serde")]
468trait Serde: serde::Serialize + for<'de> serde::Deserialize<'de> {}
469#[cfg(feature = "serde")]
470impl<T> Serde for T where T: serde::Serialize + for<'de> serde::Deserialize<'de> {}
471
472pub trait ApiVm {
473    type Arithm: StateArithm;
474    type Reader: StateReader;
475    type Adaptor: StateAdaptor;
476
477    fn vm_type(&self) -> VmType;
478}
479
480/// Reader constructs a composite state out of distinct values of all appendable state elements of
481/// the same type.
482#[allow(private_bounds)]
483pub trait StateReader: Clone + Ord + Debug + StrictDumb + StrictEncode + StrictDecode + Serde {
484    fn read<'s, I: IntoIterator<Item = &'s StateAtom>>(&self, state: impl Fn(&StateName) -> I) -> StrictVal;
485}
486
487/// Adaptors convert field elements into structured data and vise verse.
488#[allow(private_bounds)]
489pub trait StateAdaptor: Clone + Ord + Debug + StrictDumb + StrictEncode + StrictDecode + Serde {
490    fn convert_immutable(
491        &self,
492        sem_id: SemId,
493        raw_sem_id: SemId,
494        data: &StateData,
495        sys: &TypeSystem,
496    ) -> Option<StateAtom>;
497    fn convert_destructible(&self, sem_id: SemId, value: StateValue, sys: &TypeSystem) -> Option<StrictVal>;
498
499    fn build_immutable(&self, value: ConfinedBlob<0, TOTAL_BYTES>) -> StateValue;
500    fn build_destructible(&self, value: ConfinedBlob<0, TOTAL_BYTES>) -> StateValue;
501
502    fn build_state(&self, sem_id: SemId, value: StrictVal, sys: &TypeSystem) -> StateValue {
503        let typed = sys
504            .typify(value, sem_id)
505            .expect("invalid strict value not matching semantic type information");
506        let ser = sys
507            .strict_serialize_value::<TOTAL_BYTES>(&typed)
508            .expect("strict value is too large");
509        self.build_immutable(ser)
510    }
511}
512
513#[allow(private_bounds)]
514pub trait StateArithm: Clone + Debug + StrictDumb + StrictEncode + StrictDecode + Serde {
515    /// Calculator allows to perform calculations on the state (ordering and sorting, coin
516    /// selection, change calculation).
517    fn calculator(&self) -> Box<dyn StateCalc>;
518}
519
520#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display, Error)]
521#[display(doc_comments)]
522pub enum StateCalcError {
523    /// integer overflow during state computation.
524    Overflow,
525
526    /// state can't be computed.
527    UncountableState,
528}
529
530pub trait StateCalc {
531    /// Compares two state values (useful in sorting).
532    fn compare(&self, a: &StrictVal, b: &StrictVal) -> Option<Ordering>;
533
534    /// Procedure which is called on [`StateCalc`] to accumulate an input state.
535    fn accumulate(&mut self, state: &StrictVal) -> Result<(), StateCalcError>;
536
537    /// Procedure which is called on [`StateCalc`] to lessen an output state.
538    fn lessen(&mut self, state: &StrictVal) -> Result<(), StateCalcError>;
539
540    /// Procedure which is called on [`StateCalc`] to compute the difference between an input
541    /// state and output state.
542    fn diff(&self) -> Result<Vec<StrictVal>, StateCalcError>;
543
544    /// Detect whether the supplied state is enough to satisfy some target requirements.
545    fn is_satisfied(&self, state: &StrictVal) -> bool;
546}
547
548impl StateCalc for Box<dyn StateCalc> {
549    fn compare(&self, a: &StrictVal, b: &StrictVal) -> Option<Ordering> { self.as_ref().compare(a, b) }
550
551    fn accumulate(&mut self, state: &StrictVal) -> Result<(), StateCalcError> { self.as_mut().accumulate(state) }
552
553    fn lessen(&mut self, state: &StrictVal) -> Result<(), StateCalcError> { self.as_mut().lessen(state) }
554
555    fn diff(&self) -> Result<Vec<StrictVal>, StateCalcError> { self.as_ref().diff() }
556
557    fn is_satisfied(&self, state: &StrictVal) -> bool { self.as_ref().is_satisfied(state) }
558}