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

Slight rethinking of Wallet constructors (breaking change) #1500

Closed

Conversation

thunderbiscuit
Copy link
Member

@thunderbiscuit thunderbiscuit commented Jul 3, 2024

Why This PR

Our current "constructors" on the Wallet type are as follow:

1. Wallet::new()
2. Wallet::new_with_genesis_hash()
3. Wallet::load_from_changeset()
4. Wallet::new_or_load()
5. Wallet::new_or_load_with_genesis_hash()

This PR proposes a revised set of constructors offering clearer boundaries between them and their intended use:

1. Wallet::new()
2. Wallet::new_with_genesis_hash()
3. Wallet::load()
4. Wallet::load_from_changeset() -> renamed load_no_priv()

A few observations on our current set of constructors and what led me to propose this PR:

  • Wallet.new_or_load(). So... do you want to create a new wallet or load an existing one? It feels like you should know. If you don't know whether you're about to create a new wallet or load one from persistence, to me your issue lies upstream of this line in your code (but maybe there is a use case I'm not thinking of? let me know).
  • The new_or_load_with_genesis_hash() is sort of named backwards (you don't load with a specific genesis hash, you can only do that on a new wallet). The clearer name for it would be new_with_genesis_hash_or_load().
  • Loading a wallet is always done with a changeset; there is no other way to do it. In this sense, Wallet.load_from_changeset() is not optimally named. In fact, the name is also missing the core feature of the constructor: your wallet will not have private keys. An extra commit on this PR renames this method to load_no_priv(); feel free to bikeshed on the name I don't know that it's a particular good one yet and we can also just take that commit out if people like the load_from_changeset name.

Note that this PR doesn't touch the inner logic of the constructors. I simply renamed the methods, removed the Option on the changeset when calling the load methods, and fixed up the tests to break apart the behaviour tested. It did require removing the NewOrLoadError and adding the required variants to the LoadError.

Changelog notice

Breaking
    - The Wallet.new_or_load() method was removed [#1500]
    - The Wallet.load_from_changeset() method was renamed Wallet.load_no_priv() [#1500]
 
Added
    - A new method Wallet.load() was added to construct wallets [#1500]

[#1500]: https://github.com/bitcoindevkit/bdk/pull/1500

Checklists

All Submissions:

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

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

This PR removes the new_or_load Wallet constructor and requires users
to know whether they intend to load a wallet or create a new one.
…ad_no_priv

This new name makes it explicit that the wallet is being loaded without
private keys. The old name implied there was another way to load a
wallet than with changesets, which is not the case.
@LLFourn
Copy link
Contributor

LLFourn commented Jul 4, 2024

I agree this API doesn't make sense anymore after the persistence backend was remove. IMO there should just be a single new function that takes a bdk_wallet::ChangeSet.

You can have methods on bdk_wallet::ChangeSet to create an initialization ChangeSet i.e. from two descriptors and a Network and another one for a specific genesis hash. Then you persist that and load the wallet with it.

I'd do this in a PR that moves bdk_chain::CombinedChangeSet to bdk_wallet::ChangeSet and then tries to leverage the control you have over it in this crate to create a seamless new function.

@thunderbiscuit
Copy link
Member Author

thunderbiscuit commented Jul 5, 2024

A single new function that takes a bdk_wallet::ChangeSet.

Ok this is quite a departure from what we have. My initial reaction was to think that it might be odd to have to users dig into the idea of changesets before they've even made a first wallet ("darn, surely this Wallet::new method could create what I need internally?" says the newbie), but after sitting on it for 30 minutes I think you can't get away from changesets anyway, so might as well use them to their fullest.

It's more refactoring than I have here so before I get going on this it might be nice to get a Concept ACK from a few other devs on the team, but I'm up for it if that appears to be the cleanest and better way to move forward.

A few questions

  1. The current CombinedChangeSet would need to somehow hold the private keys now, but it doesn't at the moment if I understand the the indexed_tx_graph field well
pub indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>,

with the keychain::ChangeSet

pub struct ChangeSet<K> {
    /// Contains the keychains that have been added and their respective descriptor
    pub keychains_added: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
    /// Contains for each descriptor_id the last revealed index of derivation
    pub last_revealed: BTreeMap<DescriptorId, u32>,
}

Did I misunderstand your idea or maybe misunderstand the type and it does in fact have the descriptors somewhere in there that the wallet can then use to populate signers?

  1. The move of bdk_chain::CombinedChangeSet -> bdk_wallet::CombinedChangeSet is being worked on by @evanlinjin as part of Move CombinedChangeSet back to bdk_wallet::ChangeSet #1496 right? Should I just build on that Evan or is it too unstable and better to wait for it to be merged?

@notmandatory notmandatory added the api A breaking API change label Jul 5, 2024
@notmandatory notmandatory added this to the 1.0.0-alpha milestone Jul 5, 2024
@notmandatory
Copy link
Member

  1. The current CombinedChangeSet would need to somehow hold the private keys now, but it doesn't at the moment if I understand the the indexed_tx_graph field well

I'm warming up to this idea for simplifying the Wallet constructors too, but we should not add private descriptors to the change set. First off we can add new() and new_with_genesis_hash() functions to the WalletChangeSet that @evanlinjin is working on now. Those constructors can return the WalletChangeSet in a tuple with a SignerContainer. The SignerContainer would contain signers if the descriptors happen to have private keys and otherwise is empty.

@ValuedMammal
Copy link
Contributor

it might be odd to have to users dig into the idea of changesets before they've even made a first wallet

I tend to agree it seems onerous to place handling the changeset structure before creating a wallet. But I also don't hate the idea of having one new constructor that takes a changeset parameter. Given the current state of things, I would go for something like this

  fn new(desc: E, change_desc: E, network: Network) {}
  fn new_with_genesis_hash(desc: E, change_desc: E, network: Network, gen: BlockHash) {}
  fn load(cs: ChangeSet) {}
  fn load_with_descriptors(cs: ChangeSet, desc: E, change_desc: E) {}

With load it's understood that no signing keys are present. load_with_descriptors would populate signers if the keys are available. Or we just have load and make the user create their own signers container and add them with add_signers

@notmandatory
Copy link
Member

@ValuedMammal your approach is simpler for users and with docs and example should be easy to explain which Wallet constructor to use when. If you haven't already started one I'll work up some code for this API.

@evanlinjin
Copy link
Member

I like @ValuedMammal's solution. However, I would have load_with_keymaps to avoid having to create error variants of "conflicting descriptors".

What if you want to create custom signers? You can still add signers later on with add_signer (but I think it's a bad idea). It makes more sense to just sign the psbt directly.

@notmandatory
Copy link
Member

I like @ValuedMammal's solution. However, I would have load_with_keymaps to avoid having to create error variants of "conflicting descriptors".

I'd rather not make another big change to an API that bdk_wallet users are already familiar with. The load_with_descriptors constructor will fit easily for anyone who already builds their wallet with private key containing descriptors.

I'd be happy to see a complete Wallet make-over for 2.0 to enable [1..n] multi-descriptor wallets, decouple TxBuilder and coin selection, and maybe even your suggestion to only sign PSBTs outside of Wallet.

@evanlinjin
Copy link
Member

In #1514, I've introduced a builder API for constructing a Wallet.

// Load a wallet that is persisted to SQLite database.
let mut conn = bdk_wallet::rusqlite::Connection::open(file_path)?;
let mut wallet = Wallet::load()
    // check loaded descriptors matches these values and extract private keys
    .descriptors(EXTERNAL_DESC, INTERNAL_DESC)
    // you can also manually add private keys
    .keymap(KeychainKind::External, external_keymap)
    .keymap(KeychainKind::Internal, internal_keymap)
    // ensure loaded wallet's genesis hash matches this value
    .genesis_hash(genesis_hash)
    // set a lookahead for our indexer
    .lookahead(101)
    .load_wallet(&mut conn)?
    .expect("must have data to load wallet");

// Create a non-persisted wallet.
let wallet = Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
    .network(Network::Testnet)
    .create_wallet_no_persist()?;

@notmandatory
Copy link
Member

closing in favor of #1514

notmandatory added a commit that referenced this pull request Jul 22, 2024
…nstruction

64eb576 chore(wallet): Fix ChangeSet::merge (LLFourn)
8875c92 chore(wallet): Fix descriptor mismatch error keychain (LLFourn)
2cf07d6 refactor(chain,wallet)!: move rusqlite things into it's own file (志宇)
93f9b83 chore(chain): rm unused `sqlite` types (志宇)
892b97d feat(chain,wallet)!: Change persist-traits to be "safer" (志宇)
3aed4cf test(wallet): ensure checks work when loading wallet (志宇)
af4ee0f refactor(wallet)!: Make `bdk_wallet::ChangeSet` non-exhaustive (志宇)
22d02ed feat!: improve wallet building methods (志宇)
eb73f06 refactor!: move `WalletChangeSet` to `bdk_wallet` and fix import paths (志宇)
6b43001 feat!: Rework sqlite, changesets, persistence and wallet-construction (志宇)

Pull request description:

  Closes #1496
  Closes #1498
  Closes #1500

  ### Description

  Rework sqlite: Instead of only supported one schema (defined in `bdk_sqlite`), we have a schema per changeset type for more flexiblity.

  * rm `bdk_sqlite` crate (as we don't need `bdk_sqlite::Store` anymore).
  * add `sqlite` feature on `bdk_chain` which adds methods on each changeset type for initializing tables, loading the changeset and writing.

  Rework changesets: Some callers may want to use `KeychainTxOutIndex` where `K` may change per descriptor on every run. So we only want to persist the last revealed indices by `DescriptorId` (which uniquely-ish identifies the descriptor).

  * rm `keychain_added` field from `keychain_txout`'s changeset.
  * Add `keychain_added` to `CombinedChangeSet` (which is renamed to `WalletChangeSet`).

  Rework persistence: add back some safety and convenience when persisting our types. Working with changeset directly (as we were doing before) can be cumbersome.

  * Intoduce `struct Persisted<T>` which wraps a type `T` which stores staged changes to it. This adds safety when creating and or loading `T` from db.
  * `struct Persisted<T>` methods, `create`, `load` and `persist`, are available if `trait PersistWith<Db>` is implemented for `T`. `Db` represents the database connection and `PersistWith` should be implemented per database-type.
  * For async, we have `trait PersistedAsyncWith<Db>`.
  * `Wallet` has impls of `PersistedWith<rusqlite::Connection>`, `PersistedWith<rusqlite::Transaction>` and `PersistedWith<bdk_file_store::Store>` by default.

  Rework wallet-construction: Before, we had multiple methods for loading and creating with different input-counts so it would be unwieldly to add more parameters in the future. This also makes it difficult to impl `PersistWith` (which has a single method for `load` that takes in `PersistWith::LoadParams` and a single method for `create` that takes in `PersistWith::CreateParams`).

  * Introduce a builder pattern when constructing a `Wallet`. For loading from persistence or `ChangeSet`, we have `LoadParams`. For creating a new wallet, we have `CreateParams`.

  ### Notes to the reviewers

  TODO

  ### Changelog notice

  ```
  ### Added

  - Add `sqlite` feature to `bdk_chain` which introduces methods on changeset types that encode/decode changesets to SQLite database.
  * Introduce `PersistWith<Db>` and `PersistAsyncWith<Db>` traits and a `Persisted` wrapper. This ergonomically makes sure user inits the db before reading/writing to it.

  ### Changed

  - Moved `bdk_chain::CombinedChangeSet` to `bdk_wallet::ChangeSet` and added `keychain_added` field.
  - `bdk_wallet::Wallet` construction now uses a builder API using the newly introduced `CreateParams` and `LoadParams`.

  ### Removed

  - Remove `keychains_added` field from `bdk_chain::keychain_txout::ChangeSet`.

  ```
  ### 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:

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

ACKs for top commit:
  LLFourn:
    ACK: 64eb576
  notmandatory:
    Re ACK 64eb576

Tree-SHA512: b8a1d48aea26d9fa293a8387a3533cd16c8ddae890f94d61fb91efa492fb05ac5e0a66200d64d7c857774368d5f0f8991a98684307029c25f50a1d8fceee8e67
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api A breaking API change
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

5 participants