rgbstd/persistence/
inventory.rs

1// RGB standard library for working with smart contracts on Bitcoin & Lightning
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2019-2023 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2023 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22use std::collections::{BTreeMap, BTreeSet};
23use std::error::Error;
24use std::ops::Deref;
25
26use amplify::confinement::{self, Confined};
27use bp::seals::txout::blind::SingleBlindSeal;
28use bp::Txid;
29use commit_verify::mpc;
30use rgb::{
31    validation, Anchor, AnchoredBundle, BundleId, ContractId, ExposedSeal, GraphSeal, OpId,
32    Operation, Opout, SchemaId, SecretSeal, SubSchema, Transition, TransitionBundle,
33};
34use strict_encoding::TypeName;
35
36use crate::accessors::{BundleExt, MergeRevealError, RevealError};
37use crate::containers::{
38    Bindle, BuilderSeal, Cert, Consignment, ContentId, Contract, Terminal, Transfer,
39};
40use crate::interface::{
41    ContractIface, Iface, IfaceId, IfaceImpl, IfacePair, TransitionBuilder, TypedState,
42};
43use crate::persistence::hoard::ConsumeError;
44use crate::persistence::stash::StashInconsistency;
45use crate::persistence::{Stash, StashError};
46use crate::resolvers::ResolveHeight;
47use crate::Outpoint;
48
49#[derive(Debug, Display, Error, From)]
50#[display(doc_comments)]
51pub enum ConsignerError<E1: Error, E2: Error> {
52    /// unable to construct consignment: too many terminals provided.
53    TooManyTerminals,
54
55    /// unable to construct consignment: history size too large, resulting in
56    /// too many transitions.
57    TooManyBundles,
58
59    /// public state at operation output {0} is concealed.
60    ConcealedPublicState(Opout),
61
62    #[display(inner)]
63    #[from]
64    Reveal(RevealError),
65
66    #[display(inner)]
67    #[from]
68    #[from(InventoryInconsistency)]
69    InventoryError(InventoryError<E1>),
70
71    #[display(inner)]
72    #[from]
73    #[from(StashInconsistency)]
74    StashError(StashError<E2>),
75}
76
77#[derive(Debug, Display, Error, From)]
78#[display(inner)]
79pub enum InventoryError<E: Error> {
80    /// I/O or connectivity error.
81    Connectivity(E),
82
83    /// errors during consume operation.
84    // TODO: Make part of connectivity error
85    #[from]
86    Consume(ConsumeError),
87
88    /// error in input data.
89    #[from]
90    #[from(confinement::Error)]
91    DataError(DataError),
92
93    /// Permanent errors caused by bugs in the business logic of this library.
94    /// Must be reported to LNP/BP Standards Association.
95    #[from]
96    #[from(mpc::LeafNotKnown)]
97    #[from(mpc::InvalidProof)]
98    #[from(RevealError)]
99    #[from(StashInconsistency)]
100    InternalInconsistency(InventoryInconsistency),
101}
102
103impl<E1: Error, E2: Error> From<StashError<E1>> for InventoryError<E2>
104where E2: From<E1>
105{
106    fn from(err: StashError<E1>) -> Self {
107        match err {
108            StashError::Connectivity(err) => Self::Connectivity(err.into()),
109            StashError::InternalInconsistency(e) => {
110                Self::InternalInconsistency(InventoryInconsistency::Stash(e))
111            }
112        }
113    }
114}
115
116#[derive(Debug, Display, Error, From)]
117#[display(inner)]
118pub enum InventoryDataError<E: Error> {
119    /// I/O or connectivity error.
120    Connectivity(E),
121
122    /// error in input data.
123    #[from]
124    #[from(validation::Status)]
125    #[from(confinement::Error)]
126    #[from(IfaceImplError)]
127    #[from(RevealError)]
128    #[from(MergeRevealError)]
129    DataError(DataError),
130}
131
132impl<E: Error> From<InventoryDataError<E>> for InventoryError<E> {
133    fn from(err: InventoryDataError<E>) -> Self {
134        match err {
135            InventoryDataError::Connectivity(e) => InventoryError::Connectivity(e),
136            InventoryDataError::DataError(e) => InventoryError::DataError(e),
137        }
138    }
139}
140
141#[derive(Debug, Display, Error, From)]
142#[display(inner)]
143pub enum DataError {
144    /// the consignment was not validated by the local host and thus can't be
145    /// imported.
146    NotValidated,
147
148    /// consignment is invalid and can't be imported.
149    #[from]
150    Invalid(validation::Status),
151
152    /// consignment has transactions which are not known and thus the contract
153    /// can't be imported. If you are sure that you'd like to take the risc,
154    /// call `import_contract_force`.
155    UnresolvedTransactions,
156
157    /// consignment final transactions are not yet mined. If you are sure that
158    /// you'd like to take the risc, call `import_contract_force`.
159    TerminalsUnmined,
160
161    #[display(inner)]
162    #[from]
163    Reveal(RevealError),
164
165    #[from]
166    #[display(inner)]
167    Merge(MergeRevealError),
168
169    /// outpoint {0} is not part of the contract {1}.
170    OutpointUnknown(Outpoint, ContractId),
171
172    #[from]
173    Confinement(confinement::Error),
174
175    #[from]
176    IfaceImpl(IfaceImplError),
177
178    /// schema {0} doesn't implement interface {1}.
179    NoIfaceImpl(SchemaId, IfaceId),
180
181    #[from]
182    HeightResolver(Box<dyn Error>),
183
184    /// Information is concealed.
185    Concealed,
186}
187
188#[derive(Clone, Debug, Display, Error, From)]
189#[display(doc_comments)]
190pub enum IfaceImplError {
191    /// interface implementation references unknown schema {0::<0}
192    UnknownSchema(SchemaId),
193
194    /// interface implementation references unknown interface {0::<0}
195    UnknownIface(IfaceId),
196}
197
198/// These errors indicate internal business logic error. We report them instead
199/// of panicking to make sure that the software doesn't crash and gracefully
200/// handles situation, allowing users to report the problem back to the devs.
201#[derive(Debug, Display, Error, From)]
202#[display(doc_comments)]
203pub enum InventoryInconsistency {
204    /// state for contract {0} is not known or absent in the database.
205    StateAbsent(ContractId),
206
207    /// disclosure for txid {0} is absent.
208    ///
209    /// It may happen due to RGB standard library bug, or indicate internal
210    /// inventory inconsistency and compromised inventory data storage.
211    DisclosureAbsent(Txid),
212
213    /// absent information about bundle for operation {0}.
214    ///
215    /// It may happen due to RGB library bug, or indicate internal inventory
216    /// inconsistency and compromised inventory data storage.
217    BundleAbsent(OpId),
218
219    /// absent information about anchor for bundle {0}.
220    ///
221    /// It may happen due to RGB library bug, or indicate internal inventory
222    /// inconsistency and compromised inventory data storage.
223    NoBundleAnchor(BundleId),
224
225    /// the anchor is not related to the contract.
226    ///
227    /// It may happen due to RGB library bug, or indicate internal inventory
228    /// inconsistency and compromised inventory data storage.
229    #[from(mpc::LeafNotKnown)]
230    #[from(mpc::InvalidProof)]
231    UnrelatedAnchor,
232
233    /// bundle reveal error. Details: {0}
234    ///
235    /// It may happen due to RGB library bug, or indicate internal inventory
236    /// inconsistency and compromised inventory data storage.
237    #[from]
238    BundleReveal(RevealError),
239
240    /// the resulting bundle size exceeds consensus restrictions.
241    ///
242    /// It may happen due to RGB library bug, or indicate internal inventory
243    /// inconsistency and compromised inventory data storage.
244    OutsizedBundle,
245
246    #[from]
247    #[display(inner)]
248    Stash(StashInconsistency),
249}
250
251#[allow(clippy::result_large_err)]
252pub trait Inventory: Deref<Target = Self::Stash> {
253    type Stash: Stash;
254    /// Error type which must indicate problems on data retrieval.
255    type Error: Error;
256
257    fn stash(&self) -> &Self::Stash;
258
259    fn import_sigs<I>(
260        &mut self,
261        content_id: ContentId,
262        sigs: I,
263    ) -> Result<(), InventoryDataError<Self::Error>>
264    where
265        I: IntoIterator<Item = Cert>,
266        I::IntoIter: ExactSizeIterator<Item = Cert>;
267
268    fn import_schema(
269        &mut self,
270        schema: impl Into<Bindle<SubSchema>>,
271    ) -> Result<validation::Status, InventoryDataError<Self::Error>>;
272
273    fn import_iface(
274        &mut self,
275        iface: impl Into<Bindle<Iface>>,
276    ) -> Result<validation::Status, InventoryDataError<Self::Error>>;
277
278    fn import_iface_impl(
279        &mut self,
280        iimpl: impl Into<Bindle<IfaceImpl>>,
281    ) -> Result<validation::Status, InventoryDataError<Self::Error>>;
282
283    fn import_contract<R: ResolveHeight>(
284        &mut self,
285        contract: Contract,
286        resolver: &mut R,
287    ) -> Result<validation::Status, InventoryError<Self::Error>>
288    where
289        R::Error: 'static;
290
291    fn accept_transfer<R: ResolveHeight>(
292        &mut self,
293        transfer: Transfer,
294        resolver: &mut R,
295        force: bool,
296    ) -> Result<validation::Status, InventoryError<Self::Error>>
297    where
298        R::Error: 'static;
299
300    /// # Safety
301    ///
302    /// Assumes that the bundle belongs to a non-mined witness transaction. Must
303    /// be used only to consume locally-produced bundles before witness
304    /// transactions are mined.
305    fn consume_anchor(
306        &mut self,
307        anchor: Anchor<mpc::MerkleBlock>,
308    ) -> Result<(), InventoryError<Self::Error>>;
309
310    /// # Safety
311    ///
312    /// Assumes that the bundle belongs to a non-mined witness transaction. Must
313    /// be used only to consume locally-produced bundles before witness
314    /// transactions are mined.
315    fn consume_bundle(
316        &mut self,
317        contract_id: ContractId,
318        bundle: TransitionBundle,
319        witness_txid: Txid,
320    ) -> Result<(), InventoryError<Self::Error>>;
321
322    /// # Safety
323    ///
324    /// Calling this method may lead to including into the stash asset
325    /// information which may be invalid.
326    unsafe fn import_contract_force<R: ResolveHeight>(
327        &mut self,
328        contract: Contract,
329        resolver: &mut R,
330    ) -> Result<validation::Status, InventoryError<Self::Error>>
331    where
332        R::Error: 'static;
333
334    fn contracts_with_iface(
335        &mut self,
336        iface: impl Into<TypeName>,
337    ) -> Result<Vec<ContractIface>, InventoryError<Self::Error>>
338    where
339        Self::Error: From<<Self::Stash as Stash>::Error>,
340        InventoryError<Self::Error>: From<<Self::Stash as Stash>::Error>,
341    {
342        let iface = iface.into();
343        let iface_id = self.iface_by_name(&iface)?.iface_id();
344        self.contract_ids_by_iface(&iface)?
345            .into_iter()
346            .map(|id| self.contract_iface(id, iface_id))
347            .collect()
348    }
349
350    fn contract_iface_named(
351        &mut self,
352        contract_id: ContractId,
353        iface: impl Into<TypeName>,
354    ) -> Result<ContractIface, InventoryError<Self::Error>>
355    where
356        Self::Error: From<<Self::Stash as Stash>::Error>,
357        InventoryError<Self::Error>: From<<Self::Stash as Stash>::Error>,
358    {
359        let iface = iface.into();
360        let iface_id = self.iface_by_name(&iface)?.iface_id();
361        self.contract_iface(contract_id, iface_id)
362    }
363
364    fn contract_iface(
365        &mut self,
366        contract_id: ContractId,
367        iface_id: IfaceId,
368    ) -> Result<ContractIface, InventoryError<Self::Error>>;
369
370    fn anchored_bundle(&self, opid: OpId) -> Result<AnchoredBundle, InventoryError<Self::Error>>;
371
372    fn transition_builder(
373        &mut self,
374        contract_id: ContractId,
375        iface: impl Into<TypeName>,
376        transition_name: Option<impl Into<TypeName>>,
377    ) -> Result<TransitionBuilder, InventoryError<Self::Error>>
378    where
379        Self::Error: From<<Self::Stash as Stash>::Error>,
380    {
381        let schema_ifaces = self.contract_schema(contract_id)?;
382        let iface = self.iface_by_name(&iface.into())?;
383        let schema = &schema_ifaces.schema;
384        let iimpl = schema_ifaces
385            .iimpls
386            .get(&iface.iface_id())
387            .ok_or(DataError::NoIfaceImpl(schema.schema_id(), iface.iface_id()))?;
388        let builder = if let Some(transition_name) = transition_name {
389            TransitionBuilder::named_transition(
390                iface.clone(),
391                schema.clone(),
392                iimpl.clone(),
393                transition_name.into(),
394            )
395        } else {
396            TransitionBuilder::default_transition(iface.clone(), schema.clone(), iimpl.clone())
397        }
398        .expect("internal inconsistency");
399        Ok(builder)
400    }
401
402    fn blank_builder(
403        &mut self,
404        contract_id: ContractId,
405        iface: impl Into<TypeName>,
406    ) -> Result<TransitionBuilder, InventoryError<Self::Error>>
407    where
408        Self::Error: From<<Self::Stash as Stash>::Error>,
409    {
410        let schema_ifaces = self.contract_schema(contract_id)?;
411        let iface = self.iface_by_name(&iface.into())?;
412        let schema = &schema_ifaces.schema;
413        let iimpl = schema_ifaces
414            .iimpls
415            .get(&iface.iface_id())
416            .ok_or(DataError::NoIfaceImpl(schema.schema_id(), iface.iface_id()))?;
417        let builder =
418            TransitionBuilder::blank_transition(iface.clone(), schema.clone(), iimpl.clone())
419                .expect("internal inconsistency");
420        Ok(builder)
421    }
422
423    fn transition(&self, opid: OpId) -> Result<&Transition, InventoryError<Self::Error>>;
424
425    fn contracts_by_outpoints(
426        &mut self,
427        outpoints: impl IntoIterator<Item = impl Into<Outpoint>>,
428    ) -> Result<BTreeSet<ContractId>, InventoryError<Self::Error>>;
429
430    fn public_opouts(
431        &mut self,
432        contract_id: ContractId,
433    ) -> Result<BTreeSet<Opout>, InventoryError<Self::Error>>;
434
435    fn opouts_by_outpoints(
436        &mut self,
437        contract_id: ContractId,
438        outpoints: impl IntoIterator<Item = impl Into<Outpoint>>,
439    ) -> Result<BTreeSet<Opout>, InventoryError<Self::Error>>;
440
441    fn opouts_by_terminals(
442        &mut self,
443        terminals: impl IntoIterator<Item = SecretSeal>,
444    ) -> Result<BTreeSet<Opout>, InventoryError<Self::Error>>;
445
446    fn state_for_outpoints(
447        &mut self,
448        contract_id: ContractId,
449        outpoints: impl IntoIterator<Item = impl Into<Outpoint>>,
450    ) -> Result<BTreeMap<Opout, TypedState>, InventoryError<Self::Error>>;
451
452    fn store_seal_secret(&mut self, seal: GraphSeal) -> Result<(), InventoryError<Self::Error>>;
453    fn seal_secrets(&mut self) -> Result<BTreeSet<GraphSeal>, InventoryError<Self::Error>>;
454
455    #[allow(clippy::type_complexity)]
456    fn export_contract(
457        &mut self,
458        contract_id: ContractId,
459    ) -> Result<
460        Bindle<Contract>,
461        ConsignerError<Self::Error, <<Self as Deref>::Target as Stash>::Error>,
462    > {
463        let mut consignment =
464            self.consign::<GraphSeal, false>(contract_id, [] as [GraphSeal; 0])?;
465        consignment.transfer = false;
466        Ok(consignment.into())
467        // TODO: Add known sigs to the bindle
468    }
469
470    #[allow(clippy::type_complexity)]
471    fn transfer(
472        &mut self,
473        contract_id: ContractId,
474        seals: impl IntoIterator<Item = impl Into<BuilderSeal<SingleBlindSeal>>>,
475    ) -> Result<
476        Bindle<Transfer>,
477        ConsignerError<Self::Error, <<Self as Deref>::Target as Stash>::Error>,
478    > {
479        let mut consignment = self.consign(contract_id, seals)?;
480        consignment.transfer = true;
481        Ok(consignment.into())
482        // TODO: Add known sigs to the bindle
483    }
484
485    fn consign<Seal: ExposedSeal, const TYPE: bool>(
486        &mut self,
487        contract_id: ContractId,
488        seals: impl IntoIterator<Item = impl Into<BuilderSeal<Seal>>>,
489    ) -> Result<
490        Consignment<TYPE>,
491        ConsignerError<Self::Error, <<Self as Deref>::Target as Stash>::Error>,
492    > {
493        // 1. Collect initial set of anchored bundles
494        let mut opouts = self.public_opouts(contract_id)?;
495        let (outpoint_seals, terminal_seals) = seals
496            .into_iter()
497            .map(|seal| match seal.into() {
498                BuilderSeal::Revealed(seal) => (seal.outpoint(), seal.conceal()),
499                BuilderSeal::Concealed(seal) => (None, seal),
500            })
501            .unzip::<_, _, Vec<_>, Vec<_>>();
502        opouts.extend(self.opouts_by_outpoints(contract_id, outpoint_seals.into_iter().flatten())?);
503        opouts.extend(self.opouts_by_terminals(terminal_seals.iter().copied())?);
504
505        // 1.1. Get all public transitions
506        // 1.2. Collect all state transitions assigning state to the provided
507        // outpoints
508        let mut anchored_bundles = BTreeMap::<OpId, AnchoredBundle>::new();
509        let mut transitions = BTreeMap::<OpId, Transition>::new();
510        let mut terminals = BTreeMap::<BundleId, Terminal>::new();
511        for opout in opouts {
512            if opout.op == contract_id {
513                continue; // we skip genesis since it will be present anywhere
514            }
515            let transition = self.transition(opout.op)?;
516            transitions.insert(opout.op, transition.clone());
517            let anchored_bundle = self.anchored_bundle(opout.op)?;
518
519            // 2. Collect seals from terminal transitions to add to the consignment
520            // terminals
521            let bundle_id = anchored_bundle.bundle.bundle_id();
522            for (type_id, typed_assignments) in transition.assignments.iter() {
523                for index in 0..typed_assignments.len_u16() {
524                    let seal = typed_assignments.to_confidential_seals()[index as usize];
525                    if terminal_seals.contains(&seal) {
526                        terminals.insert(bundle_id, Terminal::new(seal.into()));
527                    } else if opout.no == index && opout.ty == *type_id {
528                        if let Some(seal) = typed_assignments
529                            .revealed_seal_at(index)
530                            .expect("index exists")
531                        {
532                            terminals.insert(bundle_id, Terminal::new(seal.into()));
533                        } else {
534                            return Err(ConsignerError::ConcealedPublicState(opout));
535                        }
536                    }
537                }
538            }
539
540            anchored_bundles.insert(opout.op, anchored_bundle.clone());
541        }
542
543        // 3. Collect all state transitions between terminals and genesis
544        let mut ids = vec![];
545        for transition in transitions.values() {
546            ids.extend(transition.inputs().iter().map(|input| input.prev_out.op));
547        }
548        while let Some(id) = ids.pop() {
549            if id == contract_id {
550                continue; // we skip genesis since it will be present anywhere
551            }
552            let transition = self.transition(id)?;
553            ids.extend(transition.inputs().iter().map(|input| input.prev_out.op));
554            transitions.insert(id, transition.clone());
555            anchored_bundles
556                .entry(id)
557                .or_insert(self.anchored_bundle(id)?.clone())
558                .bundle
559                .reveal_transition(transition)?;
560        }
561
562        let genesis = self.genesis(contract_id)?;
563        let schema_ifaces = self.schema(genesis.schema_id)?;
564        let mut consignment = Consignment::new(schema_ifaces.schema.clone(), genesis.clone());
565        for (iface_id, iimpl) in &schema_ifaces.iimpls {
566            let iface = self.iface_by_id(*iface_id)?;
567            consignment
568                .ifaces
569                .insert(*iface_id, IfacePair::with(iface.clone(), iimpl.clone()))
570                .expect("same collection size");
571        }
572        consignment.bundles = Confined::try_from_iter(anchored_bundles.into_values())
573            .map_err(|_| ConsignerError::TooManyBundles)?;
574        consignment.terminals =
575            Confined::try_from(terminals).map_err(|_| ConsignerError::TooManyTerminals)?;
576
577        // TODO: Conceal everything we do not need
578        // TODO: Add known sigs to the consignment
579
580        Ok(consignment)
581    }
582}