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

Fix the mess around Parity #382

Merged
merged 1 commit into from
Jan 24, 2022
Merged

Conversation

tcharding
Copy link
Member

Recently we made a wee mess with the Parity opaque type. Let's fix it
up by doing:

  • Use an enum with variants Even and Odd.
  • Add explicit conversion methods to/from u8 and i32
  • Implement BitXor

This PR attempts to follow the plan laid out in the issue but:

  • I don't get whyto_u8 is requested, I added it anyways.
  • to_i32 is not mentioned but we need it if we are going to pass the parity back to FFI code after receiving it without having to care about the value. And that is the whole point of this Parity type in the first place.
  • I don't get why from_xxx is fallible, when is an integer not even or odd?

Please note: This patch is an API breaking change that does not follow the deprecation guidelines. Rust does not allow deprecating From impl blocks AFAICT.

Fixes: #370

@dr-orlovsky
Copy link
Contributor

Not sure I am getting why we can construct parity from arbitrary integer...

src/key.rs Outdated
#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)]
pub struct Parity(i32);
pub enum Parity {
/// Ever parity.
Copy link
Contributor

Choose a reason for hiding this comment

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

s/ever/even

@elichai
Copy link
Member

elichai commented Jan 19, 2022

Not sure I am getting why we can construct parity from arbitrary integer...

I agree, I think we should only accept 0/1 and return an error otherwise (but, should those functions even exist? or should users serialize/deserialize it however they want?)

src/key.rs Outdated
Comment on lines 924 to 939
Even,
/// Odd parity.
Odd,
}
Copy link
Member

Choose a reason for hiding this comment

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

If you define

Even = 0,
Odd = 1,

then we can just cast this into a i32 using tweaked_parity as i32 idk if this casting is more or less preferred though (I know some people dislike this kind of C-like casts)

Copy link
Member

Choose a reason for hiding this comment

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

I think we should support it, it's really convenient and we don't need to have an opinion about whether it's good style.

Copy link
Member Author

Choose a reason for hiding this comment

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

FTR the cast works without explicitly setting the variant values. I have however, added the explicit variant values and removed the matching in to_u8 in favour of casting. This puts the mapping of 0/1 -> Even/Odd in a single place only.

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, a benefit of the explicit values is not relying on the language implementation.

@apoelstra
Copy link
Member

I agree with the comment suggesting we only allow 0 and 1.

I also think that, if we keep the From impls for now, we can get this into a minor release.

@tcharding
Copy link
Member Author

tcharding commented Jan 19, 2022

We can't only allow 0 and 1 (implies fallible conversion) and also keep the From impls since they are not fallible.

We could do this:

impl From<i32> for Parity {
    fn from(parity: i32) -> Parity {
        Parity::from_i32(parity).expect("Caller must ensure input is 0 or 1")
    }
}

@tcharding
Copy link
Member Author

I've implemented all suggestions. I'm not happy with the From impls, I don't think there is a good solution just choosing between two bad options

  1. Leave them in with the expect call
  2. Remove them and violate the 'don't break public API without deprecation' rule

Ideas please?

@apoelstra
Copy link
Member

@tcharding rather than implementing From in terms of Parity::from_i32, we could explicitly implement it using the `% 2' rule, and we can remove that entirely in the next major rev.

@tcharding
Copy link
Member Author

tcharding commented Jan 20, 2022

Done, added a deprecation comment as well. Cheers.

apoelstra
apoelstra previously approved these changes Jan 20, 2022
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 511b1e6

Will wait for other ACKs since I know people have opinions about this type :)

@elichai
Copy link
Member

elichai commented Jan 20, 2022

This actually still changes the behavior of the code.
if before you did let partiy = Parity::from(50); and then used it with tweak_add_check and the resulting public key is even the check will still fail because 0 != 50, after this change the check would actually pass(because we will pass 0 as parity and not 50)

https://github.com/bitcoin-core/secp256k1/blob/6815761cf5500f1a619965c5b4bbc8918b334a35/src/modules/extrakeys/main_impl.h#L151-L152

@apoelstra
Copy link
Member

Oof, subtle, thanks @elichai.

I'm tempted to just let this slide because I don't see any sensible way to work around it, and it's a fairly small behavior change that would only hit users doing invalid things.

@elichai
Copy link
Member

elichai commented Jan 20, 2022

Oof, subtle, thanks @elichai.

I'm tempted to just let this slide because I don't see any sensible way to work around it, and it's a fairly small behavior change that would only hit users doing invalid things.

I tend to agree, I doubt any user is already using the tweak_add_check and serializing parity bits (only combining these can even let you make such mistakes)

src/key.rs Outdated
Comment on lines 976 to 977
fn bitxor(self, rhs: Parity) -> Self::Output {
Parity::from_u8(self.to_u8() ^ rhs.to_u8()).expect("XOR result is always a valid parity value")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I once asked myself: Why do languages have a bitwise XOR operator but no logical XOR operator? The answer is that have one, it's !=.

Suggested change
fn bitxor(self, rhs: Parity) -> Self::Output {
Parity::from_u8(self.to_u8() ^ rhs.to_u8()).expect("XOR result is always a valid parity value")
fn bitxor(self, rhs: Parity) -> Self::Output {
self != rhs

Much simpler but the current code is of course also correct.

Copy link
Member Author

Choose a reason for hiding this comment

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

Cool, I had never thought of this. This only works in code if parity was an int and the language used 1 for true and 0 for false (i.e., C code). In Rust we'd have to map from true/false -> Parity. We can do this but I think every dev that reads the code is going to have to scribble down a truth table for XOR to convince themselves that this is the case (well I did anyways).

/// This works because Parity has only two values (i.e. only 1 bit of information).
if self == rhs {
    Parity::Even
} else {
    Parity::Odd
}

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'm unsure right now which to use?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, and of course we'd need your version because mine returned bool and not Parity. (In fact we reimplemented bool and that's not surprising: there are not many meaningful types with exactly two members.)

I still think the version with the if is more elegant than the original one. But sorry for starting this bikeshedding...

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 used the if version with some additional comments, cheers.

Copy link
Member

Choose a reason for hiding this comment

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

FWIW both compile down to a simple xor instruction: https://godbolt.org/z/5WzYd8WoY

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, I'm amazed at that assembler, all that high level language gone down to just three instructions - that's cool.

Copy link
Member

@elichai elichai Jan 24, 2022

Choose a reason for hiding this comment

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

God bless Zero Cost Abstractions ;)

Recently we made a wee mess with the `Parity` opaque type. Let's fix it
up by doing:

- Use an enum with variants `Even` and `Odd`.
- Add explicit conversion methods to/from u8 and i32
- Implement `BitXor`

Note: This patch is an API breaking change that does _not_ follow the
deprecation guidelines. Rust does not allow deprecating `From` impl
blocks AFAICT.
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 6fad20e

@apoelstra apoelstra merged commit d44646b into rust-bitcoin:master Jan 24, 2022
apoelstra added a commit that referenced this pull request Mar 9, 2022
aa51638 update changelog for 0.22.0 (Andrew Poelstra)
d06dd20 update fuzzdummy API to match normal API (Andrew Poelstra)
f3d48a2 update "should terminate abnormally" test to trigger a different ARG_CHECK (Andrew Poelstra)
8294ea3 secp256k1-sys: update upstream library (Andrew Poelstra)
2932179 secp256k1-sys: update secp256k1.h.patch (Andrew Poelstra)

Pull request description:

  Should wait on merging until we get a minor release out with #382 and #376.

  May also want to bundle #380 with this?

ACKs for top commit:
  real-or-random:
    ACK aa51638 I can't judge if the feature set is meaningful but this release PR is fine

Tree-SHA512: e7f48b402378e280a034127f2de58d3127e04303a114f07f294fa3d00c0a083ae0d43375a8a74d226b13ea45fb3fde07d8450790e602bbf9581adc5fd8bc7d29
@tcharding tcharding deleted the fix-parity branch March 24, 2022 01:37
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.

Fix the mess around Parity
6 participants