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

BlockId should not have a default and passing a chain tip should be an Option #1107 #1116

Closed
wants to merge 5 commits into from

Conversation

SpaghettiSats
Copy link

@SpaghettiSats SpaghettiSats commented Sep 11, 2023

Description

This pull request addresses the issue discussed in GitHub Issue #1107. The primary changes made in this PR are as follows:

  1. BlockId Default Removal: Removed the default implementation for BlockId as it doesn't make sense for it to have a default value. Instead, we rely on Option<BlockId> to semantically represent a null block.

  2. Chain Tip as Option: Changed the chain_tip parameter in various methods to accept an Option<BlockId> instead of using unwrap_or_default(). This aligns with Rust conventions for handling null values.

  3. ChainOracle's get_chain_tip: While keeping get_chain_tip, it is now made more meaningful by adding functionality to fetch the chain tip from the chain argument when calling certain methods. This makes the get_chain_tip method more useful and justified.

  4. API Improvement: Introduced a more convenient version of filter_chain_unspents that automatically fetches the chain tip from the chain argument, simplifying usage. The original method is retained as filter_subchain_unspents to maintain backward compatibility. Similar improvements are made for the following methods: get_chain_position, get_chain_spend, list_chain_txs, filter_chain_txouts, balance and of course also for their fallible versions.

Notes to the reviewers

  • This PR aims to improve the API by aligning it with Rust conventions for handling null values and making it more user-friendly.
  • Carefully reviewed the changes to ensure consistency with existing code and adherence to project guidelines.
  • Tests have been added for the new features and adjustments.

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

Bugfixes:

  • This pull request breaks the existing API.
  • I've added tests to reproduce the issue which are now passing (Not applicable in this case).
  • I'm linking the issue being fixed by this PR (GitHub Issue #1107).

Copy link
Contributor

@realeinherjar realeinherjar left a comment

Choose a reason for hiding this comment

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

Do we really need to .clone() all the Into<BlockId>?
Can't we pass by reference in some of these?

Comment on lines 177 to 183
chain_tip: Option<B>,
) -> Result<Option<bool>, Self::Error> {
let chain_tip: BlockId = match chain_tip {
Some(x) => x.into(),
None => return Ok(None),
};

Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't it better to return Result<bool, Self::Error>?
Result<Option<bool>, Self::Error> seems unnecessary

Copy link
Contributor

Choose a reason for hiding this comment

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

There are three possibilities:

  • Some(true): The block is in the chain
  • Some(false): The block is not in the chain
  • None we don't know if the block is in the chain.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe in the future, we can consider turning this into an enum?

Copy link
Contributor

@LLFourn LLFourn Sep 14, 2023

Choose a reason for hiding this comment

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

Not sure. Would just add documentation as far as I can see. Documenting the trait method seems clear enough. I could be wrong.

Copy link
Contributor

@LLFourn LLFourn left a comment

Choose a reason for hiding this comment

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

Thanks for doing this tedious work @SpaghettiSats. General comments:

  1. No need to add specific tests for these. Try to avoid as much as possible changes to the test files in this PR. I think it's quite hard to implement these convenience methods incorrectly.
  2. Docs need to explain what a subchain is. Liberally refer to the other method e.g. subchain_X is like [X] except it restricts the chain to a custom tip. The tip doesn't even need to be in the same chain as the tip. You can use this to find information about a point in the past or on a fork (if your chain oracle supports that).
  3. I think after looking at it we don't need to do the Into<BlockId> thing because it's already convenient enough now that we have all these variants. I'd just take a BlockId. Should address @realeinherjar's complaint above.
  4. Use the convenience methods wherever possible in wallet/mod.rs and in the tests etc

crates/bdk/src/wallet/mod.rs Outdated Show resolved Hide resolved
crates/chain/tests/test_indexed_tx_graph.rs Outdated Show resolved Hide resolved
crates/chain/tests/test_tx_graph.rs Outdated Show resolved Hide resolved
example-crates/example_cli/src/lib.rs Outdated Show resolved Hide resolved
@SpaghettiSats
Copy link
Author

Thanks for doing this tedious work @SpaghettiSats. General comments:

1. No need to add specific tests for these. Try to avoid as much as possible changes to the test files in this PR. I think it's quite hard to implement these convenience methods incorrectly.

2. Docs need to explain what a subchain is. Liberally refer to the other method e.g. `subchain_X is like [X] except it restricts the chain to a custom tip`. The tip doesn't even need to be in the same chain as the tip. You can use this to find information about a point in the past or on a fork (if your chain oracle supports that).

3. I think after looking at it we don't need to do the `Into<BlockId>` thing because it's already convenient enough now that we have all these variants. I'd just take a `BlockId`. Should address @realeinherjar's complaint above.

4. Use the convenience methods wherever possible in `wallet/mod.rs` and in the tests etc

Thanks for the feedback. I'll work on these suggestions.

Copy link
Contributor

@LLFourn LLFourn left a comment

Choose a reason for hiding this comment

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

ACK be86650

Thanks @SpaghettiSats. I think another review from someone to confirm that these extra methods are worth maintaining for the convenience they offer.

@@ -644,6 +644,11 @@ impl<A: Anchor> TxGraph<A> {

/// Get the position of the transaction in `chain` with tip `chain_tip`.
///
/// This method is like [`try_get_chain_position`] except it restricts the
/// chain to a custom tip. The tip doesn't even need to be in the same chain as the tip.
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
/// chain to a custom tip. The tip doesn't even need to be in the same chain as the tip.
/// chain to a custom tip. The tip doesn't even need to be in the same chain as the current tip.

@SpaghettiSats
Copy link
Author

I have rebased my branch and signed all the commits I have made, please if there are any other issues tell me and I will try to fix them.

@nondiremanuel nondiremanuel added this to the 1.0.0-alpha.3 milestone Sep 26, 2023
Copy link
Member

@danielabrozzoni danielabrozzoni left a comment

Choose a reason for hiding this comment

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

Thank you so much for this! I agree that removing the Default implementation on BlockId and instead using Option is much clearer, and I like the new methods that automatically fetch the chain tip from the chain oracle instead of requiring the user to provide one, they make the API way clearer.

However, I wonder whether it makes sense to have Option<BlockId> in some places. I left two comments about this, there are many other places where I'm left wondering the same thing, but I didn't comment on those as I'm probably just missing something :)

@@ -469,5 +475,80 @@ fn test_list_owned_txouts() {
confirmed: 80000 // tx1 + tx3
}
);

// Test that not specifing the chain_tip return the same results
Copy link
Member

Choose a reason for hiding this comment

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

025c4be is adding some tests that 7adfedb is removing, can you squash the two commits together instead?

@@ -17,7 +17,7 @@ pub trait ChainOracle {
fn is_block_in_chain(
&self,
block: BlockId,
chain_tip: BlockId,
chain_tip: Option<&BlockId>,
Copy link
Member

Choose a reason for hiding this comment

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

Mh, wouldn't it be better if we just had BlockId here? I can't really think of any case where someone would call is_block_in_chain(block, None), giving that it would always return None

&self,
chain: &C,
chain_tip: BlockId,
chain_tip: Option<BlockId>,
Copy link
Member

Choose a reason for hiding this comment

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

Same as my comment above, why having an Option here?

Copy link
Author

Choose a reason for hiding this comment

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

After a deeper look, I think we should keep this Option for 2 reasons: for consistency between methods and because it is convenient to check if the chain_tip is None in just one place instead of having to check every time before calling the method (the ChainOracle give an Option). If you think that is still the case to just pass the BlockId let me know and I will do it.

Copy link
Member

Choose a reason for hiding this comment

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

for consistency between methods

My point was that most (all?) of these methods should have a BlockId instead of an Option, I just commented a couple of them for brevity.

I see that it's convenient to have an Option here, but at the same time it makes the API a bit more difficult to understand.

Eventually it all comes down to personal preference, if you want to keep an Option here it's fine, but I think the decision should at least be documented properly

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I agree with @danielabrozzoni here. It causes friction to call it with the output of get_chain_tip but that friction is born by us with this PR mostly so +1.

Copy link
Author

Choose a reason for hiding this comment

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

I get it, I'll fix the problem this weekend.

@SpaghettiSats
Copy link
Author

First of all thank you for your review @danielabrozzoni. I agree with you. I will try to fix everything today.

@evanlinjin
Copy link
Member

evanlinjin commented Oct 8, 2023

Approach NACK

Thank you for taking this on and communicating clearly.

Overall, I agree that BlockId should not implement Default.

However, I think it is important that all TxGraph methods that need to fetch chain positions should still take in chain_tip: CheckPoint. Especially when multiple calls to these methods are made. The chain_tip ensures consistency in case of state changes of our ChainOracle.

Wallet::build_fee_bump is an example of how the new get_chain_position can potentially be misused. It is okay here because we are using LocalChain (which is in Wallet and we are holding a mutable reference over it). However, imagine if this is a ChainOracle implementation based on a client interface to a full/cbf node.

Additionally, now we have extra methods doing different but similar things. This increases the complexity of our API.

An Alternative Proposal:

I think we should require all ChainOracle implementations to be aware of the genesis block. LocalChain will no longer implement Default, and the caller needs to provide the genesis hash to construct it. The signature of ChainOracle::get_chain_tip would be changed to:

fn get_chain_tip(&self) -> Result<BlockId, Self::Error>;

So all TxGraph methods (which require getting chain positions) can take in chain_tip: CheckPoint (no Option), since we can guarantee to always at least have the genesis checkpoint.

The additional benefit to this, is if the caller accidentally updates LocalChain with data from the wrong network, the update can never connect (since the genesis block can't be replaced).

Related:

@LLFourn
Copy link
Contributor

LLFourn commented Oct 9, 2023

Approach NACK

Thank you for taking this on and communicating clearly.

Overall, I agree that BlockId should not implement Default.

However, I think it is important that all TxGraph methods that need to fetch chain positions should still take in chain_tip: CheckPoint. Especially when multiple calls to these methods are made. The chain_tip ensures consistency in case of state changes of our ChainOracle.

I don't understand. They don't currently take in chain_tip: CheckPoint, they take a BlockId are you proposing to change this?

So to be clear, @evanlinjin's concern can be shown clearly in this change: https://github.com/bitcoindevkit/bdk/pull/1116/files#diff-ba2cf4a27dd1b461a84dc49953334c1efe322887febbcbe6f928057c36e7f7ec

The two calls should be using the same chain tip. They are because LocalChain has a lock on it during the two calls but we don't generally guarantee this for ChainOracle so if we were to switch it then it could come up with inconsistent answers. It's difficult to see how using an old chain tip for one call and then a newer chain tip for the next call is going to be worse than using an old chain tip for both calls -- but the concern is that users will be doing this without even knowing it.

Wallet::build_fee_bump is an example of how the new get_chain_position can potentially be misused. It is okay here because we are using LocalChain (which is in Wallet and we are holding a mutable reference over it). However, imagine if this is a ChainOracle implementation based on a client interface to a full/cbf node.

I imagined it but couldn't see what exactly the problem is. What's the concern?

Additionally, now we have extra methods doing different but similar things. This increases the complexity of our API.

Convenience methods do add maintenance complexity but reduce complexity for the user. I think it depends on whether the convenience methods send the right message. If people are meant to use a method 90% of the time in one way then it makes sense to have a convenience method that simplifies that for them and reassures them that they are not "missing out" on the right way of doing something by always using it that way. I thought that's what my suggestion was doing but it actually was not. You're meant to pass in the the same block id over multiple calls to these methods to keep the answers consistent but the convenience methods stops you from doing that. Perhaps we could include more nuanced documentation on the methods to make this clearer.

An Alternative Proposal:

I think we should require all ChainOracle implementations to be aware of the genesis block. LocalChain will no longer implement Default, and the caller needs to provide the genesis hash to construct it. The signature of ChainOracle::get_chain_tip would be changed to:

fn get_chain_tip(&self) -> Result<BlockId, Self::Error>;

So all TxGraph methods (which require getting chain positions) can take in chain_tip: CheckPoint (no Option), since we can guarantee to always at least have the genesis checkpoint.

The additional benefit to this, is if the caller accidentally updates LocalChain with data from the wrong network, the update can never connect (since the genesis block can't be replaced).

Related:

* [Create LocalChain with genesis block hardwired for Network #1079](https://github.com/bitcoindevkit/bdk/issues/1079)

I don't think this is really an alternative proposal -- it's compatible with this PR also.

@evanlinjin
Copy link
Member

@LLFourn sorry I meant BlockId

@LLFourn
Copy link
Contributor

LLFourn commented Oct 9, 2023

To be clear in conclusion, I think we should not take this PR's approach for now and instead just wait on implementing @evanlinjin's proposal. I guess we can leave get_chain_tip on there. After further thinking I do see why it's useful even if we don't use it in our our API.

Sorry for originally suggesting this direction @SpaghettiSats. At least we have a better direction going forward. I think the Default on BlockId can be removed at the same time as requirement that LocalChain always be constructed with a genesis BlockId.

@LLFourn
Copy link
Contributor

LLFourn commented Nov 2, 2023

So since we are in fact going in the direction of #1079 let's close this. Once again sorry for the wasted work @SpaghettiSats. I wonder if BDK team can help prevent me from wasting other people's time like this. It seems I casually circumvented the issue validation and execution engine of the BDK team and it cost us.

An obvious suggestion is that we have ways of labeling issues that have actually been groomed by the core team that are ready for others to pick up.

Could you bring this up in the next dev meeting: @evanlinjin @notmandatory

@LLFourn LLFourn closed this Nov 2, 2023
@notmandatory
Copy link
Member

notmandatory commented Nov 13, 2023

I think for grooming new issues we should use the project "Status" field and only mark issues as "Todo" we're sure are ready and should be worked on.

@notmandatory notmandatory removed this from the 1.0.0-beta.0 milestone Nov 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

7 participants