ultrasonic/
issue.rs

1// UltraSONIC: transactional execution layer with capability-based memory access for zk-AluVM
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 core::fmt::Debug;
25use core::str::FromStr;
26
27use amplify::{ByteArray, Bytes32, Wrapper};
28use commit_verify::{
29    CommitEncode, CommitEngine, CommitId, CommitmentId, DigestExt, ReservedBytes, Sha256,
30};
31use strict_encoding::{StrictDecode, StrictDumb, StrictEncode, TypeName};
32
33use crate::{Codex, CodexId, Genesis, Identity, Opid, LIB_NAME_ULTRASONIC};
34
35/// Information on the issue of the contract.
36#[derive(Clone, Eq, Debug)]
37#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
38#[strict_type(lib = LIB_NAME_ULTRASONIC)]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
40pub struct Issue {
41    /// Version of the contract issue data structure.
42    ///
43    /// This version defines an overall contract version. Operations and Genesis have independent
44    /// versioning. Correspondence between these versions will be defined by the future consensus
45    /// rules.
46    ///
47    /// # Future use
48    ///
49    /// For now, the only supported version is one; thus, a `ReservedBytes` is used.
50    ///
51    /// In the future, with more versions coming, this should be replaced with an enum, where the
52    /// first byte will encode (with standard strict encoding) a version number as an enum variant.
53    /// For instance,
54    ///
55    /// ```ignore
56    /// pub enum Issue {
57    ///     V0(IssueV0),
58    ///     V1(IssueV1)
59    /// }
60    /// pub struct IssueV0 { /*...*/ }
61    /// pub struct IssueV1 { /*...*/ }
62    /// ```
63    pub version: ReservedBytes<1>,
64    /// Contract metadata.
65    pub meta: ContractMeta,
66    /// The codex under which the contract is issued and against which it must be validated.
67    pub codex: Codex,
68    /// Genesis operation.
69    pub genesis: Genesis,
70}
71
72impl PartialEq for Issue {
73    fn eq(&self, other: &Self) -> bool { self.commit_id() == other.commit_id() }
74}
75
76impl CommitEncode for Issue {
77    type CommitmentId = ContractId;
78
79    fn commit_encode(&self, e: &mut CommitEngine) {
80        e.commit_to_serialized(&self.version);
81        e.commit_to_serialized(&self.meta);
82        e.commit_to_serialized(&self.codex_id());
83        e.commit_to_serialized(&self.genesis.opid(ContractId::from_byte_array([0xFFu8; 32])));
84    }
85}
86
87impl Issue {
88    /// Computes contract id.
89    ///
90    /// Contract id is a commitment to the contract issue information, which includes contract
91    /// metadata, codex, and genesis operation.
92    #[inline]
93    pub fn contract_id(&self) -> ContractId { self.commit_id() }
94
95    /// Compute codex id.
96    ///
97    /// Shorthand for [`Self::issue::codex_id()`].
98    #[inline]
99    pub fn codex_id(&self) -> CodexId { self.codex.codex_id() }
100
101    /// Computes the operation id of the genesis operation.
102    ///
103    /// Equals to the [`Genesis::opid`] called with [`Self::contract_id`] as an argument.
104    #[inline]
105    pub fn genesis_opid(&self) -> Opid { self.genesis.opid(self.contract_id()) }
106}
107
108/// Consensus (layer 1) which is used by a contract.
109#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Display)]
110#[display(lowercase)]
111#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
112#[strict_type(lib = LIB_NAME_ULTRASONIC, tags = repr, into_u8, try_from_u8)]
113#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
114#[repr(u8)]
115pub enum Consensus {
116    /// No consensus is used.
117    ///
118    /// This means the contract data are not final and depend on the external consensus between
119    /// the contract parties.
120    #[strict_type(dumb)]
121    None = 0,
122
123    /// Bitcoin PoW consensus.
124    Bitcoin = 0x10,
125
126    /// Liquid federation consensus.
127    Liquid = 0x11,
128
129    /// Prime consensus.
130    Prime = 0x20,
131}
132
133impl FromStr for Consensus {
134    type Err = String;
135
136    fn from_str(s: &str) -> Result<Self, Self::Err> {
137        match s {
138            "none" => Ok(Consensus::None),
139            "bitcoin" => Ok(Consensus::Bitcoin),
140            "liquid" => Ok(Consensus::Liquid),
141            "prime" => Ok(Consensus::Prime),
142            _ => Err(s.to_owned()),
143        }
144    }
145}
146
147/// Metadata about the contract.
148#[derive(Clone, Eq, PartialEq, Debug)]
149#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
150#[strict_type(lib = LIB_NAME_ULTRASONIC)]
151#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
152pub struct ContractMeta {
153    /// Indicated whether the contract is a test contract.
154    pub testnet: bool,
155    /// Consensus layer used by the contract.
156    pub consensus: Consensus,
157    /// Timestamp of the moment the contract is issued
158    pub timestamp: i64,
159    /// A set of feature flags.
160    ///
161    /// RGB-I-0 consensus has no flags, which is enforced with [`ReservedBytes`] structure.
162    pub features: ReservedBytes<6>,
163    /// A name of the contract.
164    pub name: ContractName,
165    /// An identity of the contract issuer.
166    ///
167    /// If no identity is given, should be set to `ssi:anonymous` ([`Identity::default`]).
168    pub issuer: Identity,
169}
170
171/// Contract name.
172#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display)]
173#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
174#[strict_type(lib = LIB_NAME_ULTRASONIC, tags = custom)]
175#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))]
176pub enum ContractName {
177    /// The contract is unnamed.
178    #[strict_type(tag = 0, dumb)]
179    #[display("~")]
180    Unnamed,
181
182    /// The contract has a specific name.
183    #[strict_type(tag = 1)]
184    #[display(inner)]
185    Named(TypeName),
186}
187
188/// Unique contract identifier equivalent to the contract genesis commitment
189#[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
190#[wrapper(AsSlice, Deref, BorrowSlice, Hex, Index, RangeOps)]
191#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
192#[strict_type(lib = LIB_NAME_ULTRASONIC)]
193#[cfg_attr(
194    all(feature = "serde", not(feature = "baid64")),
195    derive(Serialize, Deserialize),
196    serde(transparent)
197)]
198pub struct ContractId(
199    #[from]
200    #[from([u8; 32])]
201    Bytes32,
202);
203
204#[cfg(all(feature = "serde", feature = "baid64"))]
205impl_serde_str_bin_wrapper!(ContractId, Bytes32);
206
207impl From<Sha256> for ContractId {
208    fn from(hasher: Sha256) -> Self { hasher.finish().into() }
209}
210
211impl CommitmentId for ContractId {
212    const TAG: &'static str = "urn:ubideco:sonic:contract#2024-11-16";
213}
214
215#[cfg(feature = "baid64")]
216mod _baid4 {
217    use core::fmt::{self, Display, Formatter};
218    use core::str::FromStr;
219
220    use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str};
221
222    use super::*;
223
224    impl DisplayBaid64 for ContractId {
225        const HRI: &'static str = "contract";
226        const CHUNKING: bool = true;
227        const PREFIX: bool = true;
228        const EMBED_CHECKSUM: bool = false;
229        const MNEMONIC: bool = false;
230        fn to_baid64_payload(&self) -> [u8; 32] { self.to_byte_array() }
231    }
232    impl FromBaid64Str for ContractId {}
233    impl FromStr for ContractId {
234        type Err = Baid64ParseError;
235        fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
236    }
237    impl Display for ContractId {
238        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
239    }
240}
241
242#[cfg(test)]
243mod test {
244    #![cfg_attr(coverage_nightly, coverage(off))]
245
246    use amplify::ByteArray;
247    use commit_verify::Digest;
248
249    use super::*;
250
251    #[test]
252    fn contract_id_display() {
253        let id = ContractId::from_byte_array(Sha256::digest(b"test"));
254        assert_eq!(format!("{id}"), "contract:n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg");
255        assert_eq!(format!("{id:-}"), "n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg");
256        assert_eq!(
257            format!("{id:#}"),
258            "contract:n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg#fractal-fashion-capsule"
259        );
260    }
261
262    #[test]
263    fn contract_id_from_str() {
264        let id = ContractId::from_byte_array(Sha256::digest(b"test"));
265        assert_eq!(
266            ContractId::from_str("contract:n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg")
267                .unwrap(),
268            id
269        );
270        assert_eq!(
271            ContractId::from_str("n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg").unwrap(),
272            id
273        );
274        assert_eq!(
275            ContractId::from_str(
276                "n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg#fractal-fashion-capsule"
277            )
278            .unwrap(),
279            id
280        );
281        assert_eq!(
282            ContractId::from_str(
283                "contract:n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg#fractal-fashion-capsule"
284            )
285            .unwrap(),
286            id
287        );
288    }
289
290    #[test]
291    #[cfg(all(feature = "serde", feature = "baid64"))]
292    fn contract_id_serde() {
293        let val = ContractId::strict_dumb();
294        test_serde_str_bin_wrapper!(
295            val,
296            "contract:AAAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA",
297            &[
298                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
299                0, 0, 0, 0
300            ]
301        );
302    }
303}