hypersonic/
ledger.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 alloc::collections::BTreeSet;
25use core::borrow::Borrow;
26use std::io;
27
28use amplify::MultiError;
29use commit_verify::{ReservedBytes, StrictHash};
30use indexmap::IndexSet;
31use sonic_callreq::MethodName;
32use sonicapi::{Api, NamedState, OpBuilder, SemanticError, Semantics, SigBlob};
33use strict_encoding::{
34    DecodeError, ReadRaw, SerializeError, StrictDecode, StrictEncode, StrictReader, StrictWriter, TypedRead, WriteRaw,
35};
36use ultrasonic::{AuthToken, CallError, CellAddr, ContractId, Identity, Issue, Operation, Opid, VerifiedOperation};
37
38use crate::deed::{CallParams, DeedBuilder};
39use crate::{Articles, EffectiveState, IssueError, ProcessedState, Stock, Transition};
40
41pub const DEEDS_VERSION: u16 = 0;
42
43/// Contract with all its state and operations, supporting updates and rollbacks.
44// We need this structure to hide internal persistence methods and not to expose them.
45// We need the persistence trait (`Stock`) in order to allow different persistence storage
46// implementations.
47#[derive(Clone, Debug)]
48pub struct Ledger<S: Stock>(S, /** Cached value */ ContractId);
49
50impl<S: Stock> Ledger<S> {
51    /// Instantiates a new contract from the provided articles, creating its persistence with the
52    /// provided configuration.
53    ///
54    /// # Panics
55    ///
56    /// This call must not panic, and instead must return an error.
57    ///
58    /// # Blocking I/O
59    ///
60    /// This call MAY perform any I/O operations.
61    pub fn new(articles: Articles, conf: S::Conf) -> Result<Self, MultiError<IssueError, S::Error>> {
62        let contract_id = articles.contract_id();
63        let state = EffectiveState::with_articles(&articles)
64            .map_err(|e| IssueError::Genesis(articles.issue().meta.name.clone(), e))
65            .map_err(MultiError::A)?;
66        let mut stock = S::new(articles, state, conf).map_err(MultiError::B)?;
67        let genesis_opid = stock.articles().genesis_opid();
68        stock.mark_valid(genesis_opid);
69        stock.commit_transaction();
70        Ok(Self(stock, contract_id))
71    }
72
73    /// Loads a contract using the provided configuration for persistence.
74    ///
75    /// # Panics
76    ///
77    /// This call must not panic, and instead must return an error.
78    ///
79    /// # Blocking I/O
80    ///
81    /// This call MAY perform any I/O operations.
82    pub fn load(conf: S::Conf) -> Result<Self, S::Error> {
83        S::load(conf).map(|stock| {
84            let contract_id = stock.articles().contract_id();
85            Self(stock, contract_id)
86        })
87    }
88
89    pub fn config(&self) -> S::Conf { self.0.config() }
90
91    pub fn stock(&self) -> &S { &self.0 }
92
93    /// Provides contract id.
94    ///
95    /// The contract id value is cached; thus, calling this operation is inexpensive.
96    ///
97    /// # Blocking I/O
98    ///
99    /// This call MUST NOT perform any I/O operations and MUST BE a non-blocking.
100    #[inline]
101    pub fn contract_id(&self) -> ContractId { self.1 }
102
103    /// Provides contract [`Articles`], which include contract genesis.
104    ///
105    /// # Blocking I/O
106    ///
107    /// This call MUST NOT perform any I/O operations and MUST BE a non-blocking.
108    #[inline]
109    pub fn articles(&self) -> &Articles { self.0.articles() }
110
111    /// Provides contract [`EffectiveState`].
112    ///
113    /// # Blocking I/O
114    ///
115    /// This call MUST NOT perform any I/O operations and MUST BE a non-blocking.
116    #[inline]
117    pub fn state(&self) -> &EffectiveState { self.0.state() }
118
119    /// Detects whether an operation with a given `opid` participates in the current state.
120    pub fn is_valid(&self, opid: Opid) -> bool { self.0.is_valid(opid) }
121
122    /// Detects whether an operation with a given `opid` is known to the contract.
123    ///
124    /// # Nota bene
125    ///
126    /// Does not include genesis operation id.
127    ///
128    /// Positive response doesn't indicate that the operation participates in the current contract
129    /// state or in a current valid contract history, which may be exported.
130    ///
131    /// Operations may be excluded from the history due to rollbacks (see [`Ledger::rollback`]),
132    /// as well as re-included later with forwards (see [`Ledger::forward`]). In both cases
133    /// they are kept in the contract storage ("stash") and remain accessible to this method.
134    ///
135    /// # Blocking I/O
136    ///
137    /// This call MAY BE blocking.
138    #[inline]
139    pub fn has_operation(&self, opid: Opid) -> bool { self.0.has_operation(opid) }
140
141    /// Returns an operation ([`Operation`]) with a given `opid` from the set of known contract
142    /// operations ("stash").
143    ///
144    /// # Nota bene
145    ///
146    /// Does not include genesis operation.
147    ///
148    /// If the method returns an operation, this doesn't indicate that the operation participates in
149    /// the current contract state or in a current valid contract history, which/ may be exported.
150    ///
151    /// Operations may be excluded from the history due to rollbacks (see [`Ledger::rollback`]),
152    /// as well as re-included later with forwards (see [`Ledger::forward`]). In both cases
153    /// they are kept in the contract storage ("stash") and remain accessible to this method.
154    ///
155    /// # Panics
156    ///
157    /// If an `opid` is not present in the contract stash, or it corresponds to the genesis
158    /// operation.
159    ///
160    /// In order to avoid panics always call the method after calling `has_operation`.
161    ///
162    /// # Blocking I/O
163    ///
164    /// This call MAY BE blocking.
165    #[inline]
166    pub fn operation(&self, opid: Opid) -> Operation { self.0.operation(opid) }
167
168    /// Returns an iterator over all operations known to the contract (i.e., the complete contract
169    /// stash).
170    ///
171    /// # Nota bene
172    ///
173    /// Does not include genesis operation.
174    ///
175    /// Contract stash is a broader concept than contract history. It includes operations which may
176    /// not contribute to the current contract state or participate in the contract history, which
177    /// may be exported.
178    ///
179    /// Operations may be excluded from the history due to rollbacks (see [`Ledger::rollback`]),
180    /// as well as re-included later with forwards (see [`Ledger::forward`]). In both cases
181    /// they are kept in the contract storage ("stash") and remain accessible to this method.
182    ///
183    /// # Panics
184    ///
185    /// The method MUST NOT panic
186    ///
187    /// # Blocking I/O
188    ///
189    /// The iterator provided in return may be a blocking iterator.
190    #[inline]
191    pub fn operations(&self) -> impl Iterator<Item = (Opid, Operation)> + use<'_, S> { self.0.operations() }
192
193    /// Returns an iterator over all state transitions known to the contract (i.e., the complete
194    /// contract trace).
195    ///
196    /// # Nota bene
197    ///
198    /// Contract trace is a broader concept than contract history. It includes state transition
199    /// which may not contribute to the current contract state or participate in the contract
200    /// history, which may be exported.
201    ///
202    /// State transitions may be excluded from the history due to rollbacks (see
203    /// [`Ledger::rollback`]), as well as re-included later with forwards (see
204    /// [`Ledger::forward`]). In both cases corresponding state transitions are kept in the
205    /// contract storage ("stash") and remain accessible to this method.
206    ///
207    /// # Panics
208    ///
209    /// The method MUST NOT panic
210    ///
211    /// # Blocking I/O
212    ///
213    /// The iterator provided in return may be a blocking iterator.
214    #[inline]
215    pub fn trace(&self) -> impl Iterator<Item = (Opid, Transition)> + use<'_, S> { self.0.trace() }
216
217    #[inline]
218    pub fn read_by(&self, addr: CellAddr) -> impl Iterator<Item = Opid> + use<'_, S> { self.0.read_by(addr) }
219    #[inline]
220    pub fn spent_by(&self, addr: CellAddr) -> Option<Opid> { self.0.spent_by(addr) }
221
222    /// # Nota bene
223    ///
224    /// Ancestors do include the original operations
225    pub fn ancestors(&self, opids: impl IntoIterator<Item = Opid>) -> impl DoubleEndedIterator<Item = Opid> {
226        let mut chain = opids.into_iter().collect::<IndexSet<_>>();
227        // Get all subsequent operations
228        let mut index = 0usize;
229        let genesis_opid = self.articles().genesis_opid();
230        while let Some(opid) = chain.get_index(index).copied() {
231            if opid != genesis_opid {
232                let op = self.0.operation(opid);
233                for inp in op.immutable_in {
234                    let parent = inp.opid;
235                    if !chain.contains(&parent) {
236                        chain.insert(parent);
237                    }
238                }
239                for inp in op.destructible_in {
240                    let parent = inp.addr.opid;
241                    if !chain.contains(&parent) {
242                        chain.insert(parent);
243                    }
244                }
245            }
246            index += 1;
247        }
248        chain.into_iter()
249    }
250
251    /// # Nota bene
252    ///
253    /// Descendants do include the original operations
254    pub fn descendants(&self, opids: impl IntoIterator<Item = Opid>) -> impl DoubleEndedIterator<Item = Opid> {
255        let mut chain = opids.into_iter().collect::<IndexSet<_>>();
256        // Get all subsequent operations
257        let mut index = 0usize;
258        while let Some(opid) = chain.get_index(index).copied() {
259            let op = self.0.operation(opid);
260            for no in 0..op.immutable_out.len_u16() {
261                let addr = CellAddr::new(opid, no);
262                for read in self.0.read_by(addr) {
263                    if !chain.contains(&read) {
264                        chain.insert(read);
265                    }
266                }
267            }
268            for no in 0..op.destructible_out.len_u16() {
269                let addr = CellAddr::new(opid, no);
270                let Some(spent) = self.0.spent_by(addr) else { continue };
271                if !chain.contains(&spent) {
272                    chain.insert(spent);
273                }
274            }
275            index += 1;
276        }
277        chain.into_iter()
278    }
279
280    /// Exports contract with all known operations.
281    pub fn export_all(&self, writer: StrictWriter<impl WriteRaw>) -> io::Result<()> {
282        self.export_internal(self.0.operation_count() as u32, writer, |_| true, |_, _, w| Ok(w))
283    }
284
285    /// Exports contract with all known operations with some auxiliary information returned by
286    /// `aux`.
287    pub fn export_all_aux<W: WriteRaw>(
288        &self,
289        writer: StrictWriter<W>,
290        aux: impl FnMut(Opid, &Operation, StrictWriter<W>) -> io::Result<StrictWriter<W>>,
291    ) -> io::Result<()> {
292        self.export_internal(self.0.operation_count() as u32, writer, |_| true, aux)
293    }
294
295    /// Export a part of a contract history: a graph between a set of terminals and genesis.
296    pub fn export(
297        &self,
298        terminals: impl IntoIterator<Item = impl Borrow<AuthToken>>,
299        writer: StrictWriter<impl WriteRaw>,
300    ) -> io::Result<()> {
301        self.export_aux(terminals, writer, |_, _, w| Ok(w))
302    }
303
304    /// Exports contract and operations to a stream, extending operation data with some auxiliary
305    /// information returned by `aux`.
306    pub fn export_aux<W: WriteRaw>(
307        &self,
308        terminals: impl IntoIterator<Item = impl Borrow<AuthToken>>,
309        writer: StrictWriter<W>,
310        aux: impl FnMut(Opid, &Operation, StrictWriter<W>) -> io::Result<StrictWriter<W>>,
311    ) -> io::Result<()> {
312        let mut queue = terminals
313            .into_iter()
314            .map(|terminal| self.0.state().addr(*terminal.borrow()).opid)
315            .collect::<BTreeSet<_>>();
316        let articles = self.articles();
317        let genesis_opid = articles.genesis_opid();
318        queue.remove(&genesis_opid);
319        let mut opids = queue.clone();
320        while let Some(opid) = queue.pop_first() {
321            let st = self.0.transition(opid);
322            for prev in st.destroyed.into_keys().map(|a| a.opid) {
323                if !opids.contains(&prev) && prev != genesis_opid {
324                    opids.insert(prev);
325                    queue.insert(prev);
326                }
327            }
328        }
329
330        // Include all operations defining published state
331        let state = self.state();
332        let mut collect = |api: &Api, state: &ProcessedState| {
333            for (state_name, owned) in &api.global {
334                if owned.published {
335                    let Some(cells) = state.global.get(state_name) else {
336                        continue;
337                    };
338                    opids.extend(cells.keys().map(|addr| addr.opid));
339                }
340            }
341        };
342        collect(&articles.semantics().default, &state.main);
343        for (api_name, api) in &articles.semantics().custom {
344            let Some(state) = state.aux.get(api_name) else {
345                continue;
346            };
347            collect(api, state);
348        }
349        opids.remove(&genesis_opid);
350
351        self.export_internal(opids.len() as u32, writer, |opid| opids.remove(opid), aux)?;
352
353        debug_assert!(
354            opids.is_empty(),
355            "Missing operations: {}",
356            opids
357                .into_iter()
358                .map(|opid| opid.to_string())
359                .collect::<Vec<_>>()
360                .join("\n -")
361        );
362
363        Ok(())
364    }
365
366    /// Exports only operations for which `should_include` returns `true`.
367    ///
368    /// # Nota bene
369    ///
370    /// Does not write the contract id.
371    pub fn export_internal<W: WriteRaw>(
372        &self,
373        count: u32,
374        mut writer: StrictWriter<W>,
375        mut should_include: impl FnMut(&Opid) -> bool,
376        mut aux: impl FnMut(Opid, &Operation, StrictWriter<W>) -> io::Result<StrictWriter<W>>,
377    ) -> io::Result<()> {
378        let articles = self.articles();
379        let genesis_opid = articles.genesis_opid();
380
381        // Write version number
382        writer = (DEEDS_VERSION as u8).strict_encode(writer)?;
383        // Write contract id
384        let contract_id = self.contract_id();
385        writer = self.contract_id().strict_encode(writer)?;
386        // Write an empty extension block
387        writer = 0u8.strict_encode(writer)?;
388        // Write articles
389        writer = articles.strict_encode(writer)?;
390        writer = aux(genesis_opid, &articles.genesis().to_operation(contract_id), writer)?;
391        // Write no of operations
392        writer = count.strict_encode(writer)?;
393        // Stream operations
394        for (opid, op) in self.0.operations() {
395            if !should_include(&opid) {
396                continue;
397            }
398            writer = op.strict_encode(writer)?;
399            writer = aux(opid, &op, writer)?;
400        }
401        Ok(())
402    }
403
404    pub fn upgrade_apis(&mut self, new_articles: Articles) -> Result<bool, MultiError<SemanticError, S::Error>> {
405        self.0
406            .update_articles(|articles| articles.upgrade_apis(new_articles))
407    }
408
409    pub fn accept<E>(
410        &mut self,
411        reader: &mut StrictReader<impl ReadRaw>,
412        sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
413    ) -> Result<(), MultiError<AcceptError, S::Error>> {
414        // We need this closure to avoid multiple `map_err`.
415        let count = (|| -> Result<u32, AcceptError> {
416            // Check version number
417            let _ = ReservedBytes::<1, { DEEDS_VERSION as u8 }>::strict_decode(reader)?;
418
419            let contract_id = ContractId::strict_decode(reader)?;
420
421            // Read and ignore the extension block
422            let ext_blocks = u8::strict_decode(reader)?;
423            for _ in 0..ext_blocks {
424                let len = u16::strict_decode(reader)?;
425                let r = unsafe { reader.raw_reader() };
426                let _ = r.read_raw::<{ u16::MAX as usize }>(len as usize)?;
427            }
428
429            // Read articles
430            let semantics = Semantics::strict_decode(reader)?;
431            let sig = Option::<SigBlob>::strict_decode(reader)?;
432            let issue = Issue::strict_decode(reader)?;
433            let articles = Articles::with(semantics, issue, sig, sig_validator)?;
434            if articles.contract_id() != contract_id {
435                return Err(AcceptError::Articles(SemanticError::ContractMismatch));
436            }
437
438            self.upgrade_apis(articles)
439                .map_err(|e| AcceptError::Persistence(e.to_string()))?;
440
441            let count = u32::strict_decode(reader)?;
442            Ok(count)
443        })()
444        .map_err(MultiError::A)?;
445
446        // We need to account for genesis, which is not included in the `count`
447        for _ in 0..=count {
448            let op = match Operation::strict_decode(reader) {
449                Ok(operation) => operation,
450                Err(DecodeError::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => break,
451                Err(e) => return Err(MultiError::A(e.into())),
452            };
453            self.apply_verify(op, false)?;
454        }
455        // Here we do not check for the end of the stream,
456        // so in the future we can have arbitrary extensions
457        // put here with no backward compatibility issues.
458        self.commit_transaction();
459        Ok(())
460    }
461
462    pub fn rollback(&mut self, opids: impl IntoIterator<Item = Opid>) -> Result<(), S::Error> {
463        for opid in self.descendants(opids).rev() {
464            let mut transition = self.0.transition(opid);
465            // We need to filter out already invalidated inputs
466            let inputs = transition
467                .destroyed
468                .keys()
469                .copied()
470                .collect::<IndexSet<_>>();
471            for addr in inputs {
472                if !self.is_valid(addr.opid) {
473                    // empty destroyed is allowed
474                    let _ = transition.destroyed.remove(&addr);
475                }
476            }
477            self.0.update_state(|state, articles| {
478                state.rollback(transition, articles.semantics());
479            })?;
480            self.0.mark_invalid(opid);
481        }
482        self.commit_transaction();
483        Ok(())
484    }
485
486    pub fn forward(&mut self, opids: impl IntoIterator<Item = Opid>) -> Result<(), MultiError<AcceptError, S::Error>> {
487        for opid in self.descendants(opids) {
488            debug_assert!(!self.is_valid(opid));
489            if self
490                .ancestors([opid])
491                .filter(|id| *id != opid)
492                .all(|id| self.is_valid(id))
493            {
494                let op = self.0.operation(opid);
495                self.apply_verify(op, true)?;
496                debug_assert!(self.is_valid(opid));
497            }
498        }
499        self.commit_transaction();
500        Ok(())
501    }
502
503    pub fn start_deed(&mut self, method: impl Into<MethodName>) -> DeedBuilder<'_, S> {
504        let builder = OpBuilder::new(self.contract_id(), self.0.articles().call_id(method));
505        DeedBuilder { builder, ledger: self }
506    }
507
508    pub fn call(&mut self, params: CallParams) -> Result<Opid, MultiError<AcceptError, S::Error>> {
509        let mut builder = self.start_deed(params.core.method);
510
511        for NamedState { name, state } in params.core.global {
512            builder = builder.append(name, state.verified, state.unverified);
513        }
514        for NamedState { name, state } in params.core.owned {
515            builder = builder.assign(name, state.auth, state.data, state.lock);
516        }
517        for addr in params.reading {
518            builder = builder.reading(addr);
519        }
520        for (addr, satisfaction) in params.using {
521            if let Some(satisfaction) = satisfaction {
522                builder = builder.satisfying(addr, satisfaction.name, satisfaction.witness);
523            } else {
524                builder = builder.using(addr);
525            }
526        }
527
528        builder.commit()
529    }
530
531    /// Adds operation which was already checked to the stock. This does the following:
532    /// - includes raw operation to stash;
533    /// - computes state modification and applies it to the state;
534    /// - saves removed state as a [`Transition`] and adds it to the execution trace.
535    ///
536    /// # Returns
537    ///
538    /// Whether the operation was already successfully included (`true`), or was already present in
539    /// the stash.
540    ///
541    /// # Nota bene
542    ///
543    /// It is required to call [`Self::commit_transaction`] after all calls to this method.
544    pub fn apply_verify(
545        &mut self,
546        operation: Operation,
547        force: bool,
548    ) -> Result<bool, MultiError<AcceptError, S::Error>> {
549        if operation.contract_id != self.contract_id() {
550            return Err(MultiError::A(AcceptError::Articles(SemanticError::ContractMismatch)));
551        }
552
553        let opid = operation.opid();
554
555        let present = self.0.is_valid(opid);
556        let articles = self.0.articles();
557        if !present || force {
558            let verified = articles
559                .codex()
560                .verify(self.contract_id(), operation, &self.0.state().raw, articles)
561                .map_err(AcceptError::from)
562                .map_err(MultiError::A)?;
563            self.apply_internal(opid, verified, present && !force)
564                .map_err(MultiError::B)?;
565        }
566
567        Ok(present)
568    }
569
570    /// Adds operation which was already checked to the stock. This does the following:
571    /// - includes raw operation to stash;
572    /// - computes state modification and applies it to the state;
573    /// - saves removed state as a [`Transition`] and adds it to the execution trace.
574    ///
575    /// # Returns
576    ///
577    /// State invalidated by the operation in the form of a [`Transition`].
578    ///
579    /// # Nota bene
580    ///
581    /// It is required to call [`Self::commit_transaction`] after all calls to this method.
582    pub fn apply(&mut self, operation: VerifiedOperation) -> Result<Transition, S::Error> {
583        let opid = operation.opid();
584        let present = self.0.is_valid(opid);
585        self.apply_internal(opid, operation, present)
586    }
587
588    fn apply_internal(
589        &mut self,
590        opid: Opid,
591        operation: VerifiedOperation,
592        present: bool,
593    ) -> Result<Transition, S::Error> {
594        if !present {
595            self.0.add_operation(opid, operation.as_operation());
596        }
597
598        let op = operation.as_operation();
599        for read in &op.immutable_in {
600            self.0.add_reading(*read, opid);
601        }
602        for prevout in &op.destructible_in {
603            self.0.add_spending(prevout.addr, opid);
604        }
605
606        let transition = self
607            .0
608            .update_state(|state, articles| state.apply(operation, articles.semantics()))?;
609
610        self.0.add_transition(opid, &transition);
611        self.0.mark_valid(opid);
612        Ok(transition)
613    }
614
615    pub fn commit_transaction(&mut self) { self.0.commit_transaction(); }
616}
617
618#[derive(Debug, Display, Error, From)]
619#[display(inner)]
620pub enum AcceptError {
621    #[from]
622    Io(io::Error),
623
624    #[from]
625    Articles(SemanticError),
626
627    #[from]
628    Verify(CallError),
629
630    #[from]
631    Decode(DecodeError),
632
633    #[from]
634    Serialize(SerializeError),
635
636    Persistence(String),
637
638    #[cfg(feature = "binfile")]
639    #[display("Invalid file format")]
640    InvalidFileFormat,
641}
642
643#[cfg(feature = "binfile")]
644mod _fs {
645    use std::path::Path;
646
647    use binfile::BinFile;
648    use strict_encoding::{StreamReader, StreamWriter};
649
650    use super::*;
651
652    pub const DEEDS_MAGIC_NUMBER: u64 = u64::from_be_bytes(*b"DEEDLDGR");
653
654    impl<S: Stock> Ledger<S> {
655        pub fn export_all_to_file(&self, output: impl AsRef<Path>) -> io::Result<()> {
656            let file = BinFile::<DEEDS_MAGIC_NUMBER, DEEDS_VERSION>::create_new(output)?;
657            let writer = StrictWriter::with(StreamWriter::new::<{ usize::MAX }>(file));
658            self.export_all(writer)
659        }
660
661        pub fn export_to_file(
662            &self,
663            terminals: impl IntoIterator<Item = impl Borrow<AuthToken>>,
664            output: impl AsRef<Path>,
665        ) -> io::Result<()> {
666            let file = BinFile::<DEEDS_MAGIC_NUMBER, DEEDS_VERSION>::create_new(output)?;
667            let writer = StrictWriter::with(StreamWriter::new::<{ usize::MAX }>(file));
668            self.export(terminals, writer)
669        }
670
671        pub fn accept_from_file<E>(
672            &mut self,
673            input: impl AsRef<Path>,
674            sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
675        ) -> Result<(), MultiError<AcceptError, S::Error>> {
676            let file = BinFile::<DEEDS_MAGIC_NUMBER, DEEDS_VERSION>::open(input)
677                .map_err(|_| AcceptError::InvalidFileFormat)
678                .map_err(MultiError::from_a)?;
679            let mut reader = StrictReader::with(StreamReader::new::<{ usize::MAX }>(file));
680            self.accept(&mut reader, sig_validator)
681        }
682    }
683}
684#[cfg(feature = "binfile")]
685pub use _fs::*;