sonicapi/state/
adaptors.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 std::io;
25
26use aluvm::LibSite;
27use amplify::confinement::{Confined, ConfinedBlob};
28use amplify::num::u256;
29use sonic_callreq::StateName;
30use strict_encoding::{SerializeError, StreamReader};
31use strict_types::value::{EnumTag, StrictNum};
32use strict_types::{decode, typify, Cls, SemId, StrictVal, Ty, TypeSystem};
33use ultrasonic::StateValue;
34
35use crate::{fe256, StateTy, LIB_NAME_SONIC};
36
37pub(super) const USED_FIEL_BYTES: usize = u256::BYTES as usize - 2;
38pub(super) const MAX_BYTES: usize = USED_FIEL_BYTES * 3;
39
40#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
41#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
42#[strict_type(lib = LIB_NAME_SONIC, tags = custom, dumb = Self::TypedEncoder(strict_dumb!()))]
43#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
44pub enum StateConvertor {
45    #[strict_type(tag = 0x00)]
46    Unit,
47
48    #[strict_type(tag = 0x10)]
49    TypedEncoder(StateTy),
50
51    #[strict_type(tag = 0x11)]
52    TypedFieldEncoder(StateTy),
53    // In the future we can add more adaptors:
54    // - doing more compact encoding (storing state type in bits, not using a full field element);
55    // - using just a specific range of field element bits, not a full value - such that multiple APIs may read
56    //   different parts of the same data;
57    /// Execute a custom function.
58    // AluVM is reserved for the future. We need it here to avoid breaking changes.
59    #[strict_type(tag = 0xFF)]
60    AluVM(
61        /// The entry point to the script (virtual machine uses libraries from
62        /// [`crate::Semantics`]).
63        LibSite,
64    ),
65}
66
67impl StateConvertor {
68    pub fn convert(
69        &self,
70        sem_id: SemId,
71        value: StateValue,
72        sys: &TypeSystem,
73    ) -> Result<Option<StrictVal>, StateConvertError> {
74        match self {
75            Self::Unit if StateValue::None == value => Ok(Some(StrictVal::Unit)),
76            Self::Unit => Err(StateConvertError::UnitState),
77            Self::TypedEncoder(ty) => typed_convert(*ty, sem_id, value, sys),
78            Self::TypedFieldEncoder(ty) => typed_field_convert(*ty, sem_id, value, sys),
79            Self::AluVM(_) => Err(StateConvertError::Unsupported),
80        }
81    }
82}
83
84#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
85#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
86#[strict_type(lib = LIB_NAME_SONIC, tags = custom, dumb = Self::TypedEncoder(strict_dumb!()))]
87#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
88pub enum StateBuilder {
89    #[strict_type(tag = 0x00)]
90    Unit,
91
92    #[strict_type(tag = 0x10)]
93    TypedEncoder(StateTy),
94
95    #[strict_type(tag = 0x11)]
96    TypedFieldEncoder(StateTy),
97    // In the future we can add more adaptors:
98    // - doing more compact encoding (storing state type in bits, not using a full field element);
99    /// Execute a custom function.
100    // AluVM is reserved for the future. We need it here to avoid breaking changes.
101    #[strict_type(tag = 0xFF)]
102    AluVM(
103        /// The entry point to the script (virtual machine uses libraries from
104        /// [`crate::Semantics`]).
105        LibSite,
106    ),
107}
108
109impl StateBuilder {
110    #[allow(clippy::result_large_err)]
111    pub fn build(&self, sem_id: SemId, value: StrictVal, sys: &TypeSystem) -> Result<StateValue, StateBuildError> {
112        let typed = sys.typify(value.clone(), sem_id)?;
113        Ok(match self {
114            Self::Unit if typed.as_val() == &StrictVal::Unit => StateValue::None,
115            Self::Unit => return Err(StateBuildError::InvalidUnit),
116            Self::TypedEncoder(ty) => {
117                let ser = sys.strict_serialize_value::<MAX_BYTES>(&typed)?;
118                typed_build(*ty, ser)
119            }
120            Self::TypedFieldEncoder(ty) => typed_field_build(*ty, value)?,
121            Self::AluVM(_) => return Err(StateBuildError::Unsupported),
122        })
123    }
124}
125
126#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
127#[display(inner)]
128pub enum StateBuildError {
129    #[display("unknown state name '{0}'")]
130    UnknownStateName(StateName),
131
132    #[from]
133    Typify(typify::Error),
134
135    #[from(io::Error)]
136    #[display("state data is too large to be encoded")]
137    TooLarge,
138
139    #[display("state data ({0:?}) have an unsupported type for the encoding")]
140    UnsupportedValue(StrictVal),
141
142    #[from]
143    Serialize(SerializeError),
144
145    #[display("the provided value doesn't match the required unit type")]
146    InvalidUnit,
147
148    #[display("AluVM is not yet supported for a state builder.")]
149    Unsupported,
150}
151
152#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
153pub enum StateConvertError {
154    #[display("unknown state name '{0}'")]
155    UnknownStateName(StateName),
156
157    #[from]
158    #[display(inner)]
159    Decode(decode::Error),
160
161    #[display("state value is not fully consumed")]
162    NotEntirelyConsumed,
163
164    #[display("state has no data")]
165    UnitState,
166
167    #[display("unknown type {0}")]
168    TypeUnknown(SemId),
169
170    #[display("type of class {0} is not supported by field-based convertor")]
171    TypeClassUnsupported(Cls),
172
173    #[display("number of fields doesn't match the number of fields in the type")]
174    TypeFieldCountMismatch,
175
176    #[display("AluVM is not yet supported for a state conversion.")]
177    Unsupported,
178}
179
180// Simplify newtype-like tuples
181fn reduce_tuples(mut val: StrictVal) -> StrictVal {
182    loop {
183        if let StrictVal::Tuple(ref mut vec) = val {
184            if vec.len() == 1 {
185                val = vec.remove(0);
186                continue;
187            }
188        }
189        return val;
190    }
191}
192
193fn typed_convert(
194    ty: StateTy,
195    sem_id: SemId,
196    value: StateValue,
197    sys: &TypeSystem,
198) -> Result<Option<StrictVal>, StateConvertError> {
199    let from_ty = value.get(0).ok_or(StateConvertError::UnitState)?.to_u256();
200    // State type does not match
201    if from_ty != ty {
202        return Ok(None);
203    }
204
205    let mut buf = [0u8; MAX_BYTES];
206    let mut i = 1u8;
207    while let Some(el) = value.get(i) {
208        let from = USED_FIEL_BYTES * (i - 1) as usize;
209        let to = USED_FIEL_BYTES * i as usize;
210        buf[from..to].copy_from_slice(&el.to_u256().to_le_bytes()[..USED_FIEL_BYTES]);
211        i += 1;
212    }
213    let used_bytes = USED_FIEL_BYTES * (i - 1) as usize;
214    debug_assert!(i <= 4);
215    debug_assert!(used_bytes <= MAX_BYTES);
216
217    let mut cursor = StreamReader::cursor::<MAX_BYTES>(&buf[..used_bytes]);
218    let mut val = sys.strict_read_type(sem_id, &mut cursor)?.unbox();
219
220    // We check here that we have reached the end of the buffer data,
221    // and the rest of the elements are zeros.
222    let cursor = cursor.unconfine();
223    let position = cursor.position() as usize;
224    let data = cursor.into_inner();
225    for item in data.iter().take(used_bytes).skip(position) {
226        if *item != 0 {
227            return Err(StateConvertError::NotEntirelyConsumed);
228        }
229    }
230
231    val = reduce_tuples(val);
232
233    Ok(Some(val))
234}
235
236fn typed_field_convert(
237    ty: StateTy,
238    sem_id: SemId,
239    value: StateValue,
240    sys: &TypeSystem,
241) -> Result<Option<StrictVal>, StateConvertError> {
242    let from_ty = value.get(0).ok_or(StateConvertError::UnitState)?.to_u256();
243    // State type does not match
244    if from_ty != ty {
245        return Ok(None);
246    }
247
248    let ty = sys
249        .get(sem_id)
250        .ok_or(StateConvertError::TypeUnknown(sem_id))?;
251    let fields = match ty {
252        Ty::Tuple(fields) => fields.iter().copied().collect::<Vec<SemId>>(),
253        Ty::Struct(fields) => fields.iter().map(|f| f.ty).collect::<Vec<SemId>>(),
254        _ => return Err(StateConvertError::TypeClassUnsupported(ty.cls())),
255    };
256
257    if fields.len() != value.into_iter().count() - 1 {
258        return Err(StateConvertError::TypeFieldCountMismatch);
259    }
260
261    let mut items = vec![];
262    for (el, sem_id) in value.into_iter().skip(1).zip(fields.into_iter()) {
263        let mut cursor = StreamReader::cursor::<MAX_BYTES>(el.to_u256().to_le_bytes());
264        let val = sys.strict_read_type(sem_id, &mut cursor)?.unbox();
265        items.push(val);
266    }
267
268    let mut val = match ty {
269        Ty::Tuple(_) => StrictVal::Tuple(items),
270        Ty::Struct(fields) => StrictVal::Struct(
271            fields
272                .iter()
273                .zip(items)
274                .map(|(f, val)| (f.name.clone(), reduce_tuples(val)))
275                .collect(),
276        ),
277        _ => unreachable!(),
278    };
279
280    // Simplify tuples with a single element
281    val = reduce_tuples(val);
282
283    Ok(Some(val))
284}
285
286fn typed_build(ty: StateTy, ser: ConfinedBlob<0, MAX_BYTES>) -> StateValue {
287    let mut elems = Vec::with_capacity(4);
288    elems.push(ty);
289    for chunk in ser.chunks(USED_FIEL_BYTES) {
290        let mut buf = [0u8; u256::BYTES as usize];
291        buf[..chunk.len()].copy_from_slice(chunk);
292        elems.push(u256::from_le_bytes(buf));
293    }
294
295    StateValue::from_iter(elems)
296}
297
298#[allow(clippy::result_large_err)]
299fn typed_field_build(ty: StateTy, val: StrictVal) -> Result<StateValue, StateBuildError> {
300    let mut elems = Vec::with_capacity(4);
301    elems.push(ty);
302
303    Ok(match val {
304        StrictVal::Unit => StateValue::Single { first: fe256::from(ty) },
305        StrictVal::Number(StrictNum::Uint(i)) => StateValue::Double { first: fe256::from(ty), second: fe256::from(i) },
306        StrictVal::String(s) if s.len() < MAX_BYTES => {
307            typed_build(ty, Confined::from_iter_checked(s.as_bytes().iter().cloned()))
308        }
309        StrictVal::Bytes(b) if b.len() < MAX_BYTES => typed_build(ty, Confined::from_checked(b.0)),
310        StrictVal::Struct(fields) if fields.len() <= 3 => typed_field_build_items(ty, fields.into_values())?,
311        StrictVal::Enum(EnumTag::Ord(tag)) => StateValue::Double { first: fe256::from(ty), second: fe256::from(tag) },
312        StrictVal::List(items) | StrictVal::Set(items) | StrictVal::Tuple(items) if items.len() <= 3 => {
313            typed_field_build_items(ty, items)?
314        }
315        _ => return Err(StateBuildError::UnsupportedValue(val)),
316    })
317}
318
319#[allow(clippy::result_large_err)]
320fn typed_field_build_items(
321    ty: StateTy,
322    vals: impl IntoIterator<Item = StrictVal>,
323) -> Result<StateValue, StateBuildError> {
324    let mut items = Vec::with_capacity(4);
325    items.push(ty);
326    for val in vals {
327        if let Some(val) = typed_field_build_item(val)? {
328            items.push(val);
329        }
330    }
331    Ok(StateValue::from_iter(items))
332}
333
334#[allow(clippy::result_large_err)]
335fn typed_field_build_item(val: StrictVal) -> Result<Option<u256>, StateBuildError> {
336    Ok(match val {
337        StrictVal::Unit => None,
338        StrictVal::Tuple(items) if items.len() == 1 => typed_field_build_item(items[0].clone())?,
339        StrictVal::Number(StrictNum::Uint(i)) => Some(u256::from(i)),
340        StrictVal::String(s) if s.len() < USED_FIEL_BYTES => {
341            let mut buf = [0u8; u256::BYTES as usize];
342            buf[..s.len()].copy_from_slice(s.as_bytes());
343            Some(u256::from_le_bytes(buf))
344        }
345        StrictVal::Bytes(b) if b.len() < USED_FIEL_BYTES => {
346            let mut buf = [0u8; u256::BYTES as usize];
347            buf[..b.len()].copy_from_slice(&b.0);
348            Some(u256::from_le_bytes(buf))
349        }
350        StrictVal::Enum(EnumTag::Ord(tag)) => Some(u256::from(tag)),
351        _ => return Err(StateBuildError::UnsupportedValue(val)),
352    })
353}
354
355#[cfg(test)]
356mod tests {
357    #![cfg_attr(coverage_nightly, coverage(off))]
358
359    use strict_types::stl::std_stl;
360    use strict_types::{LibBuilder, SymbolicSys, SystemBuilder, TypeLib};
361
362    use super::*;
363
364    pub const LIB_NAME_TEST: &str = "Test";
365
366    #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, From)]
367    #[display(lowercase)]
368    #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
369    #[strict_type(lib = LIB_NAME_TEST, tags = repr, try_from_u8, into_u8)]
370    #[repr(u8)]
371    pub enum Vote {
372        #[strict_type(dumb)]
373        Contra = 0,
374        Pro = 1,
375    }
376
377    #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, From)]
378    #[display(inner)]
379    #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
380    #[strict_type(lib = LIB_NAME_TEST)]
381    pub struct VoteId(u64);
382
383    #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, From)]
384    #[display(inner)]
385    #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
386    #[strict_type(lib = LIB_NAME_TEST)]
387    pub struct PartyId(u64);
388
389    #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From, Display)]
390    #[display("Participant #{party_id} voted {vote} in voting #{vote_id}")]
391    #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
392    #[strict_type(lib = LIB_NAME_TEST)]
393    pub struct CastVote {
394        pub vote_id: VoteId,
395        pub vote: Vote,
396        pub party_id: PartyId,
397    }
398
399    pub fn stl() -> TypeLib {
400        LibBuilder::with(libname!(LIB_NAME_TEST), [std_stl().to_dependency_types()])
401            .transpile::<CastVote>()
402            .compile()
403            .expect("invalid Test type library")
404    }
405
406    #[derive(Debug)]
407    pub struct Types(SymbolicSys);
408
409    impl Types {
410        pub fn new() -> Self {
411            Self(
412                SystemBuilder::new()
413                    .import(std_stl())
414                    .unwrap()
415                    .import(stl())
416                    .unwrap()
417                    .finalize()
418                    .unwrap(),
419            )
420        }
421
422        pub fn type_system(&self) -> TypeSystem {
423            let stdtypes = std_stl().types;
424            let types = stl().types;
425            let types = stdtypes
426                .into_iter()
427                .chain(types)
428                .map(|(tn, ty)| ty.sem_id_named(&tn));
429            self.0.as_types().extract(types).unwrap()
430        }
431
432        pub fn get(&self, name: &'static str) -> SemId {
433            *self
434                .0
435                .resolve(name)
436                .unwrap_or_else(|| panic!("type '{name}' is absent in RGB21 type library"))
437        }
438    }
439
440    fn typed_roundtrip(name: &'static str, src: StateValue, dst: StrictVal) {
441        let types = Types::new();
442
443        let ty = types.get(name);
444        let val = StateConvertor::TypedEncoder(u256::ONE)
445            .convert(ty, src, &types.type_system())
446            .unwrap()
447            .unwrap();
448        assert_eq!(val, dst);
449
450        let res = StateBuilder::TypedEncoder(u256::ONE)
451            .build(ty, dst, &types.type_system())
452            .unwrap();
453        assert_eq!(res, src);
454    }
455
456    fn typed_field_roundtrip(name: &'static str, src1: StateValue, dst: StrictVal, src2: StrictVal) {
457        let types = Types::new();
458
459        let ty = types.get(name);
460        let val = StateConvertor::TypedFieldEncoder(u256::ONE)
461            .convert(ty, src1, &types.type_system())
462            .unwrap()
463            .unwrap();
464        assert_eq!(val, dst);
465
466        let res = StateBuilder::TypedFieldEncoder(u256::ONE)
467            .build(ty, src2, &types.type_system())
468            .unwrap();
469        assert_eq!(res, src1);
470    }
471
472    #[test]
473    fn typed() {
474        typed_roundtrip(
475            "Std.Bool",
476            StateValue::Double { first: fe256::from(1u8), second: fe256::from(1u8) },
477            svenum!("true"),
478        );
479    }
480
481    #[test]
482    #[should_panic(expected = "Decode(Decode(Io(Kind(UnexpectedEof))))")]
483    fn typed_convert_lack() {
484        let types = Types::new();
485        StateConvertor::TypedEncoder(u256::ONE)
486            .convert(types.get("Std.Bool"), StateValue::Single { first: fe256::from(1u8) }, &types.type_system())
487            .unwrap();
488    }
489
490    #[test]
491    #[should_panic(expected = "NotEntirelyConsumed")]
492    fn typed_convert_excess() {
493        let types = Types::new();
494        StateConvertor::TypedEncoder(u256::ONE)
495            .convert(
496                types.get("Std.Bool"),
497                StateValue::Triple {
498                    first: fe256::from(1u8),
499                    second: fe256::from(1u8),
500                    third: fe256::from(1u8),
501                },
502                &types.type_system(),
503            )
504            .unwrap();
505    }
506
507    #[test]
508    fn typed_field() {
509        typed_field_roundtrip(
510            "Test.CastVote",
511            StateValue::Quadruple {
512                first: fe256::from(1u8),
513                second: fe256::from(3u8),
514                third: fe256::from(1u8),
515                fourth: fe256::from(5u8),
516            },
517            ston!(voteId 3u8, vote svenum!("pro"), partyId 5u8),
518            ston!(voteId 3u8, vote svenum!(1), partyId 5u8),
519        );
520    }
521
522    #[test]
523    #[should_panic(expected = "TypeClassUnsupported(Enum)")]
524    fn typed_field_convert_enum() {
525        let types = Types::new();
526        let val = StateConvertor::TypedFieldEncoder(u256::ONE)
527            .convert(
528                types.get("Std.Bool"),
529                StateValue::Double { first: fe256::from(1u8), second: fe256::from(1u8) },
530                &types.type_system(),
531            )
532            .unwrap();
533        assert_eq!(val, Some(svenum!("true")));
534    }
535
536    #[test]
537    #[should_panic(expected = "TypeFieldCountMismatch")]
538    fn typed_field_convert_lack() {
539        let types = Types::new();
540        StateConvertor::TypedFieldEncoder(u256::ONE)
541            .convert(types.get("Test.CastVote"), StateValue::Single { first: fe256::from(1u8) }, &types.type_system())
542            .unwrap();
543    }
544
545    #[test]
546    #[should_panic(expected = "TypeFieldCountMismatch")]
547    fn typed_field_convert_excess() {
548        let types = Types::new();
549        StateConvertor::TypedFieldEncoder(u256::ONE)
550            .convert(
551                types.get("Test.PartyId"),
552                StateValue::Triple {
553                    first: fe256::from(1u8),
554                    second: fe256::from(1u8),
555                    third: fe256::from(1u8),
556                },
557                &types.type_system(),
558            )
559            .unwrap();
560    }
561
562    #[test]
563    #[should_panic(
564        expected = r#"Decode(Decode(EnumTagNotKnown("semid:kr1DHi~j-YSw4n54-o9KnZ9Q-Dlo0pWP-_V9U5oh-Wlzfemk#break-secret-delphi", 5)))"#
565    )]
566    fn typed_field_convert_invalid() {
567        let types = Types::new();
568        StateConvertor::TypedFieldEncoder(u256::ONE)
569            .convert(
570                types.get("Test.CastVote"),
571                StateValue::Quadruple {
572                    first: fe256::from(1u8),
573                    second: fe256::from(1u8),
574                    third: fe256::from(5u8),
575                    fourth: fe256::from(1u8),
576                },
577                &types.type_system(),
578            )
579            .unwrap();
580    }
581}