Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify wallet persistence #1547

Merged

Conversation

evanlinjin
Copy link
Member

@evanlinjin evanlinjin commented Aug 11, 2024

Description

Removed the persistence module in bdk_chain (which contained the PersistWith and PersistAsyncWith traits and a Persisted<T> wrapper). Replaced it with simplified versions that are Wallet-specific.

The new traits (WalletPersister and AsyncWalletPersister) are simpler since they have less type-parameters (being wallet-specific). The old versions were more complex as they were intended to be used with any type. However, we need more time to finalize a works-for-all-bdk-types persistence API.

Additionally, WalletPersister and AsyncWalletPersister also introduce the initialize method. It is handy to contain db-initialization (i.e. create tables for SQL) here. We can call initialize in PersistedWallet constructors. So the PersistedWallet::persist method does not need to ensure the database is initialized. To accommodate this, I made the .init_sqlite_table methods on changeset-types public, and removed the call of this in from_sqlite and .persist_to_sqlite. The initialize method now loads from the persister, so the load associated function is removed.

There was a bug in the old PersistAsyncWith trait where the lifetime bounds were not strict enough (refer to the conversation in #1552). This is now fixed in the new AsyncWalletPersister trait.

Docs for the new types are clearer (hopefully). I mentioned implementation details about the new traits and what safety checks were guaranteed in PersistedWallet.

I think it makes sense just to have a wallet-specific persistence API which we will maintain alongside wallet for v1.0. This is less baggage for users and maintainers alike.

Notes to the reviewers

How breaking are these changes?

Unless if you were implementing your own persistence, then not breaking at all! If you were implementing your own persistence for BDK, the breakages are minimal. The main change is introducing the initialize method to persistence traits (which replaces load).

Acknowledgements

I came up with the idea of this while responding to @tnull's suggestions on Discord. Unfortunately we cannot fulfill all of his requests. However, I do believe that this will make things easier for our users.

Changelog notice

  • Removed the persist module in bdk_chain.
  • Added WalletPersister/AsyncWalletPersister traits and PersistedWallet struct to bdk_wallet. These are simplified and safer versions of old structs provided by the persist module in bdk_chain.
  • Change .init_sqlite_tables method to be public on changeset types. from_sqlite and persist_into_sqlite methods no longer call .init_sqlite_tables internally.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

What is left to do:

  • Better docs.

@notmandatory
Copy link
Member

Concept ACK

Even though we need to stop making breaking changes, this looks like like a reasonable isolated change that will simplify the persistence API for Wallet users.

@evanlinjin evanlinjin force-pushed the simplify_wallet_persistence branch 3 times, most recently from eee5efa to 79f1051 Compare August 14, 2024 10:58
@notmandatory
Copy link
Member

LGTM, needs fmt, rebase on master, and commits cleanup. I tested with wallet and cli examples and worked fine, including loading existing db created with prior version.

@evanlinjin evanlinjin force-pushed the simplify_wallet_persistence branch 2 times, most recently from cd7eeab to c6962fb Compare August 15, 2024 03:48
This replaces `bdk_chain::PersistWith` which wanted to persist anything
(not only `Wallet`), hence, requiring a whole bunch of generic
parameters.

Having `WalletPersister` dedicated to persisting `Wallet` simplifies the
trait by a lot.

In addition, `AsyncWalletPersister` has proper lifetime bounds whereas
`bdk_chain::PersistAsyncWith` did not.
Changeset methods `.persist_to_sqlite` and `from_sqlite` no longer
internally call `.init_sqlite_tables`. Instead, it is up to the caller
to call `.init_sqlite_tables` beforehand.

This allows us to utilize `WalletPersister::initialize`, instead of
calling `.init_sqlite_tables` every time we persist/load.
Introduce `Wallet::staged_mut` method.
This forces the caller to use the same persister type that they used for
loading/creating when calling `.persist` on `PersistedWallet`.

This is not totally fool-proof since we can have multiple instances of
the same persister type persisting to different databases. However, it
does further enforce some level of safety.
@evanlinjin evanlinjin marked this pull request as ready for review August 15, 2024 06:06
@evanlinjin
Copy link
Member Author

An idea would be to introduce some sort of test framework for WalletPersister and AsyncWalletPersister implementations.

Checks include:

  • Calling initialize multiple times should not error, or make database inconsistent.
  • Calling initialize after persist should not error, or make database inconsistent.
  • What previously went in should come out.

Maybe a more elaborate version of

fn wallet_is_persisted() -> anyhow::Result<()> {
?

Copy link
Member

@notmandatory notmandatory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick review and found a few doc fixes but otherwise looks good. I'll do a deeper look this afternoon but so far looks ready to go.

crates/wallet/src/wallet/params.rs Outdated Show resolved Hide resolved
crates/wallet/src/wallet/params.rs Outdated Show resolved Hide resolved
crates/wallet/src/wallet/params.rs Outdated Show resolved Hide resolved
crates/wallet/src/wallet/params.rs Outdated Show resolved Hide resolved
crates/wallet/src/wallet/persisted.rs Outdated Show resolved Hide resolved
crates/wallet/src/wallet/persisted.rs Outdated Show resolved Hide resolved
crates/wallet/src/wallet/persisted.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@ValuedMammal ValuedMammal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a first pass. Also spent some time implementing AsyncWalletPersister for a locally defined type and so far results are positive.

@@ -2312,6 +2303,15 @@ impl Wallet {
}
}

/// Get a mutable reference of the staged [`ChangeSet`] that is yet to be commited (if any).
pub fn staged_mut(&mut self) -> Option<&mut ChangeSet> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn staged_mut(&mut self) -> Option<&mut ChangeSet> {
pub(crate) fn staged_mut(&mut self) -> Option<&mut ChangeSet> {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that this method may be useful for some users. I.e. they don't need to query the staged area twice if they wanted to do something like we do in PersistedWallet::persist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of discussion I think you can do it without ever getting the stage mutably such as having a method on Wallet that resets the stage but doesn't return it, and this would prevent users from freely modifying their changesets.

  /// Clears the stage.
  pub fn clear_stage(&mut self) { self.stage = ChangeSet::default(); }

Then persist would look like

  /// Persist staged changes.
  pub fn persist(&mut self, persister: &mut P) -> Result<bool, P::Error> {
      if let Some(change) = self.inner.staged() {
          P::persist(persister, change)?;
          self.inner.clear_stage();
          return Ok(true);
      }
      Ok(false)
  }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I think it's a better approach to have/use a clear_stage method, having explicit and clear meaning.

Comment on lines +91 to +96
fn persist<'a>(
persister: &'a mut Self,
changeset: &'a ChangeSet,
) -> FutureResult<'a, (), Self::Error>
where
Self: 'a;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if this would achieve the same thing

Suggested change
fn persist<'a>(
persister: &'a mut Self,
changeset: &'a ChangeSet,
) -> FutureResult<'a, (), Self::Error>
where
Self: 'a;
fn persist<'a>(&'a mut self, changeset: &'a ChangeSet) -> FutureResult<'a, (), Self::Error>;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I think we still need Self: 'a.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I made purposely made persist a non-member so that it's harder to call haha

crates/wallet/src/wallet/persisted.rs Show resolved Hide resolved
crates/wallet/src/wallet/persisted.rs Show resolved Hide resolved
crates/wallet/src/wallet/persisted.rs Show resolved Hide resolved
@evanlinjin
Copy link
Member Author

@ValuedMammal @notmandatory I updated the docs! Let me know if everything makes sense.

I'll be disappearing this weekend and be back Tuesday night. Feel free to push on commits for more doc fixes and merge (if appropriate).

crates/wallet/src/wallet/persisted.rs Outdated Show resolved Hide resolved
crates/wallet/src/wallet/persisted.rs Outdated Show resolved Hide resolved
@oleonardolima
Copy link
Contributor

Concept ACK
It's looking good to me. I guess I found only two documentation fixes.

Copy link
Contributor

@ValuedMammal ValuedMammal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK f434c93 (pending comments by @oleonardolima )

/// persister implementations may NOT require initialization at all (and not error).
///
/// [`persist`]: WalletPersister::persist
fn initialize(persister: &mut Self) -> Result<ChangeSet, Self::Error>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nit:

It might be more convenient for the implementer if the return type is Result<Option<ChangeSet>, Self::Error>.

Copy link
Member

@notmandatory notmandatory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 340808e

@notmandatory notmandatory merged commit acccb59 into bitcoindevkit:master Aug 19, 2024
20 checks passed
@notmandatory notmandatory mentioned this pull request Aug 25, 2024
31 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

4 participants