sonicapi/
api.rs

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