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

Improve address error handling #1950

Closed

Conversation

tcharding
Copy link
Member

@tcharding tcharding commented Jul 21, 2023

Improve the address::Error using ideas from @sanket1729 here: rust-bitcoin/rust-secp256k1#633

EDIT: This proof-of-concept is now done by: #1971

This PR is now a proof of concept for a new way of providing error types to users of the library. Specifically

  • Functions return specific error types that are either structs or enums where every variant is used
  • Modules provide a general error type that includes a variant for each of the specific error types and From impls so users of the lib can call multiple functions from a module and use ? if they do not wish to handle then specific errors.

The address module is a good module to use to test this new idea out because it is in flux anyway and is complicated enough to need multiple errors. Note also that existence of the new PayloadError should make upcoming work to hide the Payload type easier.

Notes

Since this PR introduces a new convention for error code please be picky about the small things, describe in the individual commit logs (eg, using error submodule, the import/export style).

apoelstra
apoelstra previously approved these changes Jul 21, 2023
Copy link
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

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

ACK 5f161be

sanket1729
sanket1729 previously approved these changes Jul 23, 2023
Copy link
Member

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

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

ACK 5f161be. This does create minor issue when we don't have a common unifying type for users who just want to bubble up the error.

let addr = Address::from_str()?; // Error type = Parse
let _ = addr.some_other_api()?; // Error type = Error

Now the downstream user who wants to return a Result for this function has to create a common unifying type for this.

ACK the current changes, just noting the issue as mentioned. If we deem this important. We can add another variant to general Address API, that captures this and make a from/into impl

@apoelstra
Copy link
Member

Oh, yeah, that's a good point. I think that Error should have a new Parse variant that holds this, so that users can unify the types.

@sanket1729
Copy link
Member

Motivated me to write this: rust-bitcoin/rust-secp256k1#633

In preparation for improving error handling in the `address` module move
`address.rs` to `address/mod.rs`, add an `address/error.rs` file, and
move the public error type to the new module.

Note:

- Make there module private, this is a code organization thing only.
- Use wildcard re-export of `self::error::*` in `address/mod.rs`
- Use `super` to import things from `address` into `error`

Refactor only, no logic changes.
In order to make the code slightly more terse with no loss of meaning
import all the error variants locally in the `Display` impl for
`address::Error`.

Refactor only, no logic changes.
In error `source` function be explicit about what is a reference, match
on `*self` and use `ref` when pattern matching.
The `address` currently has a single large error enum that is returned
by all functions irrespective of whether the function can fail with all
variants or not.

Improve the address error handling by doing:

- Create a general purpose error type `address::Error` that has a
variant for each of the other error variants in the module. This type is
not currently returned by any of our functions but is provided for users
of the library who wish to call various `address` functions and do not
care to match on the specific error type.

- Create specific error types for functions so that each function
returns an error that is either a struct or an enum in which all
variants are used.
fn from(e: witness_program::Error) -> Error { Error::WitnessProgram(e) }
}
mod error;
pub use self::error::*;
Copy link
Member

Choose a reason for hiding this comment

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

In a91b00d:

Thanks for highlighting this in the commit message. But I think we should drop it and just directly import Error. If there are other types that need importing we'll notice it when we try to use them. And right now there is only one type in the module.

Copy link
Member

Choose a reason for hiding this comment

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

I see, in a later commit you add more types. But I still think they should be listed explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can do, in hindsight I should have realised you'd hold this position :) Is there, in your opinion, ever a valid use of wildcard imports? I'm not being facetious, legitimate question.

Copy link
Member

Choose a reason for hiding this comment

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

In my opinion, no. I don't mind use Error::* in functions that match on error variants, since it's extremely clear what's going on, but in basically every other case, they just break the ability to grep for stuff.


impl From<base58::Error> for ParseError {
fn from(e: base58::Error) -> Self { Self::Base58(e) }
}
Copy link
Member

Choose a reason for hiding this comment

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

In bd8cdc1:

You also need a From<base58::Error> for Error (and same for all the other for ParseError impls).

Copy link
Member Author

Choose a reason for hiding this comment

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

If that is the case then I am misunderstanding the point of the Error type. As I understood it the type was for users of the lib to use as a return value for functions they write that call multiple functions from the address module/type. There is no use of it in the codebase on this branch. Or am I missing something?

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @apoelstra. For some reason I thought that .into() would do a transitive call from base58::Error -> ParseError -> Error, but it does not. (and for good reasons!)

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=4ee5670908a74cd6fdf709799f8cf2be

Copy link
Member

Choose a reason for hiding this comment

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

I am reconsidering this approach as a whole now. It is not true that base58::Error must go via address::Error. Consider the following code where this leads to surprising behaviour because of our from impl.

pub fn user_fn() -> Result<(), address::Error> {
    // This will convert to address::Error variant, but this has nothing to do with address. 
	let base58_parsed = bytes::from_base58()?;
}

I guess we should only do it for obvious cases where we are clearly defined error heararchy. (which is not the case with base58).

Copy link
Member

Choose a reason for hiding this comment

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

As I understood it the type was for users of the lib to use as a return value for functions they write that call multiple functions from the address module/type.

Right, but say they call multiple functions from the module and also call a function that returns a base58::Error. Now they need to write ?? (which won't compile) or .map_err(Error::from)? (which is ugly).

Copy link
Member

Choose a reason for hiding this comment

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

I guess we should only do it for obvious cases where we are clearly defined error heararchy. (which is not the case with base58).

Ah, yeah, I do agree with this. I had the same thought/fear when working this out in my head.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, but say they call multiple functions from the module and also call a function that returns a base58::Error.

I would think in this case the user should be defining a new error type or just using anyhow::Result. Actually now I write this only libs create error types anyway and library writers using rust-bitcoin will be used to creating error types, they do not need this to be in rust-bitcoin. Application devs just use anyhow so they don't need it either.

Copy link
Member

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

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

I am starting to see the issue with this approach. Suddenly, we will have an error type for each API and that can blow up. But I still think that this is the correct thing to do.

Given the extra amount of code that has to be written for this, I completely understand if we don't want to do it this way.

}
Ok(Payload::ScriptHash(script.script_hash()))
}

/// Create a witness pay to public key payload from a public key
pub fn p2wpkh(pk: &PublicKey) -> Result<Payload, Error> {
pub fn p2wpkh(pk: &PublicKey) -> Result<Payload, PayloadError> {
Copy link
Member

Choose a reason for hiding this comment

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

I was expecting this to return only UncompressedPubKeyError, not the entire PayloadError.

Copy link
Member Author

Choose a reason for hiding this comment

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

Wow, you are totally correct and this might not be a good module to test run this new error convention on because loads of the Payload functions need changing. I'm going to convert this to draft and go find an easier module to proof this now convention with.

@apoelstra
Copy link
Member

Suddenly, we will have an error type for each API and that can blow up. But I still think that this is the correct thing to do.

It won't actually be that much more code vs consolidating, since every error variant has to go somewhere. It'll result in more match self { and } lines mostly.

If we also implement From::from more agressively it will also add log(n) times as many From impls but that's fine, they're short.

I would think in this case the user should be defining a new error type or just using anyhow::Result. Actually now I write this only libs create error types anyway

This is definitely not true. People writing applications use real error types in their utility functons, and being forced to convert to anyhow is super annoying when rust-bitcoin already has a general enough error type to cover your use-case, the authors just didn't implement From so you can't actually use it without contortions.

@tcharding
Copy link
Member Author

Righto, I haven't fully internalised the reasoning but I'll run with it for a bit and see if it sinks in. I'll just make a macro for the from impls.

@tcharding
Copy link
Member Author

stale

@tcharding tcharding closed this Feb 5, 2024
@tcharding tcharding deleted the 07-21-cleanup-address branch May 22, 2024 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants