Skip to content

Commit

Permalink
Merge bitcoindevkit#965: Implement persistence with the new structures
Browse files Browse the repository at this point in the history
4963240 Add more `impl`s for `Append` and docs for file store `magic` (志宇)
2aa08a5 [persist_redesign] Introduce redesigned `persist` types (志宇)

Pull request description:

  ### Description

  This is part of bitcoindevkit#895 and bitcoindevkit#971

  * Introduce a more generic version of the `keychain::persist::*` structures that only needs a single generic for the changeset type.

  Additional changes:

  * The `Append` trait has a new method `is_empty`.
  * Introduce `Store` structure for `bdk_file_store` (which implements `PersistBackend`).

  ### Changelog notice

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature

Top commit has no ACKs.

Tree-SHA512: 0211fbe7d7e27805d3ed3a80b42f184cdff1cebb32fd559aa9838e4a7f7c7e47b6c366b6ef68e299f876bafed549b8d1d8b8cc0366bf5b61db079504a565b9b4
  • Loading branch information
evanlinjin committed May 10, 2023
2 parents e3c1370 + 4963240 commit 05d353c
Show file tree
Hide file tree
Showing 10 changed files with 548 additions and 100 deletions.
4 changes: 4 additions & 0 deletions crates/chain/src/indexed_tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ impl<A: Anchor, IA: Append> Append for IndexedAdditions<A, IA> {
self.graph_additions.append(other.graph_additions);
self.index_additions.append(other.index_additions);
}

fn is_empty(&self) -> bool {
self.graph_additions.is_empty() && self.index_additions.is_empty()
}
}

/// Represents a structure that can index transaction data.
Expand Down
4 changes: 4 additions & 0 deletions crates/chain/src/keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ impl<K: Ord> Append for DerivationAdditions<K> {

self.0.append(&mut other.0);
}

fn is_empty(&self) -> bool {
self.0.is_empty()
}
}

impl<K> Default for DerivationAdditions<K> {
Expand Down
2 changes: 2 additions & 0 deletions crates/chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub mod tx_graph;
pub use tx_data_traits::*;
mod chain_oracle;
pub use chain_oracle::*;
mod persist;
pub use persist::*;

#[doc(hidden)]
pub mod example_utils;
Expand Down
89 changes: 89 additions & 0 deletions crates/chain/src/persist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use core::convert::Infallible;

use crate::Append;

/// `Persist` wraps a [`PersistBackend`] (`B`) to create a convenient staging area for changes (`C`)
/// before they are persisted.
///
/// Not all changes to the in-memory representation needs to be written to disk right away, so
/// [`Persist::stage`] can be used to *stage* changes first and then [`Persist::commit`] can be used
/// to write changes to disk.
#[derive(Debug)]
pub struct Persist<B, C> {
backend: B,
stage: C,
}

impl<B, C> Persist<B, C>
where
B: PersistBackend<C>,
C: Default + Append,
{
/// Create a new [`Persist`] from [`PersistBackend`].
pub fn new(backend: B) -> Self {
Self {
backend,
stage: Default::default(),
}
}

/// Stage a `changeset` to be commited later with [`commit`].
///
/// [`commit`]: Self::commit
pub fn stage(&mut self, changeset: C) {
self.stage.append(changeset)
}

/// Get the changes that have not been commited yet.
pub fn staged(&self) -> &C {
&self.stage
}

/// Commit the staged changes to the underlying persistance backend.
///
/// Returns a backend-defined error if this fails.
pub fn commit(&mut self) -> Result<(), B::WriteError> {
let mut temp = C::default();
core::mem::swap(&mut temp, &mut self.stage);
self.backend.write_changes(&temp)
}
}

/// A persistence backend for [`Persist`].
///
/// `C` represents the changeset; a datatype that records changes made to in-memory data structures
/// that are to be persisted, or retrieved from persistence.
pub trait PersistBackend<C> {
/// The error the backend returns when it fails to write.
type WriteError: core::fmt::Debug;

/// The error the backend returns when it fails to load changesets `C`.
type LoadError: core::fmt::Debug;

/// Writes a changeset to the persistence backend.
///
/// It is up to the backend what it does with this. It could store every changeset in a list or
/// it inserts the actual changes into a more structured database. All it needs to guarantee is
/// that [`load_from_persistence`] restores a keychain tracker to what it should be if all
/// changesets had been applied sequentially.
///
/// [`load_from_persistence`]: Self::load_from_persistence
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;

/// Return the aggregate changeset `C` from persistence.
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError>;
}

impl<C: Default> PersistBackend<C> for () {
type WriteError = Infallible;

type LoadError = Infallible;

fn write_changes(&mut self, _changeset: &C) -> Result<(), Self::WriteError> {
Ok(())
}

fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> {
Ok(C::default())
}
}
37 changes: 37 additions & 0 deletions crates/chain/src/tx_data_traits.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::collections::BTreeMap;
use crate::collections::BTreeSet;
use crate::BlockId;
use alloc::vec::Vec;
use bitcoin::{Block, OutPoint, Transaction, TxOut};

/// Trait to do something with every txout contained in a structure.
Expand Down Expand Up @@ -64,20 +65,56 @@ impl<A: Anchor> Anchor for &'static A {
pub trait Append {
/// Append another object of the same type onto `self`.
fn append(&mut self, other: Self);

/// Returns whether the structure is considered empty.
fn is_empty(&self) -> bool;
}

impl Append for () {
fn append(&mut self, _other: Self) {}

fn is_empty(&self) -> bool {
true
}
}

impl<K: Ord, V> Append for BTreeMap<K, V> {
fn append(&mut self, mut other: Self) {
BTreeMap::append(self, &mut other)
}

fn is_empty(&self) -> bool {
BTreeMap::is_empty(self)
}
}

impl<T: Ord> Append for BTreeSet<T> {
fn append(&mut self, mut other: Self) {
BTreeSet::append(self, &mut other)
}

fn is_empty(&self) -> bool {
BTreeSet::is_empty(self)
}
}

impl<T> Append for Vec<T> {
fn append(&mut self, mut other: Self) {
Vec::append(self, &mut other)
}

fn is_empty(&self) -> bool {
Vec::is_empty(self)
}
}

impl<A: Append, B: Append> Append for (A, B) {
fn append(&mut self, other: Self) {
Append::append(&mut self.0, other.0);
Append::append(&mut self.1, other.1);
}

fn is_empty(&self) -> bool {
Append::is_empty(&self.0) && Append::is_empty(&self.1)
}
}
7 changes: 7 additions & 0 deletions crates/chain/src/tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,13 @@ impl<A: Ord> Append for Additions<A> {
.collect::<Vec<_>>(),
);
}

fn is_empty(&self) -> bool {
self.tx.is_empty()
&& self.txout.is_empty()
&& self.anchors.is_empty()
&& self.last_seen.is_empty()
}
}

impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
Expand Down
100 changes: 100 additions & 0 deletions crates/file_store/src/entry_iter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use bincode::Options;
use std::{
fs::File,
io::{self, Seek},
marker::PhantomData,
};

use crate::bincode_options;

/// Iterator over entries in a file store.
///
/// Reads and returns an entry each time [`next`] is called. If an error occurs while reading the
/// iterator will yield a `Result::Err(_)` instead and then `None` for the next call to `next`.
///
/// [`next`]: Self::next
pub struct EntryIter<'t, T> {
db_file: Option<&'t mut File>,

/// The file position for the first read of `db_file`.
start_pos: Option<u64>,
types: PhantomData<T>,
}

impl<'t, T> EntryIter<'t, T> {
pub fn new(start_pos: u64, db_file: &'t mut File) -> Self {
Self {
db_file: Some(db_file),
start_pos: Some(start_pos),
types: PhantomData,
}
}
}

impl<'t, T> Iterator for EntryIter<'t, T>
where
T: serde::de::DeserializeOwned,
{
type Item = Result<T, IterError>;

fn next(&mut self) -> Option<Self::Item> {
// closure which reads a single entry starting from `self.pos`
let read_one = |f: &mut File, start_pos: Option<u64>| -> Result<Option<T>, IterError> {
let pos = match start_pos {
Some(pos) => f.seek(io::SeekFrom::Start(pos))?,
None => f.stream_position()?,
};

match bincode_options().deserialize_from(&*f) {
Ok(changeset) => {
f.stream_position()?;
Ok(Some(changeset))
}
Err(e) => {
if let bincode::ErrorKind::Io(inner) = &*e {
if inner.kind() == io::ErrorKind::UnexpectedEof {
let eof = f.seek(io::SeekFrom::End(0))?;
if pos == eof {
return Ok(None);
}
}
}
f.seek(io::SeekFrom::Start(pos))?;
Err(IterError::Bincode(*e))
}
}
};

let result = read_one(self.db_file.as_mut()?, self.start_pos.take());
if result.is_err() {
self.db_file = None;
}
result.transpose()
}
}

impl From<io::Error> for IterError {
fn from(value: io::Error) -> Self {
IterError::Io(value)
}
}

/// Error type for [`EntryIter`].
#[derive(Debug)]
pub enum IterError {
/// Failure to read from the file.
Io(io::Error),
/// Failure to decode data from the file.
Bincode(bincode::ErrorKind),
}

impl core::fmt::Display for IterError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
IterError::Io(e) => write!(f, "io error trying to read entry {}", e),
IterError::Bincode(e) => write!(f, "bincode error while reading entry {}", e),
}
}
}

impl std::error::Error for IterError {}
Loading

0 comments on commit 05d353c

Please sign in to comment.