Skip to content

bip32: infallible derive_pub#2911

Closed
storopoli wants to merge 2 commits intorust-bitcoin:masterfrom
storopoli:bip32/derive_pub-unfallible
Closed

bip32: infallible derive_pub#2911
storopoli wants to merge 2 commits intorust-bitcoin:masterfrom
storopoli:bip32/derive_pub-unfallible

Conversation

@storopoli
Copy link
Copy Markdown
Contributor

@storopoli storopoli commented Jun 24, 2024

Closes #2752.

@storopoli storopoli changed the title bip32: infallible derive_pub bip32: infallible derive_pub Jun 24, 2024
@github-actions github-actions bot added the C-bitcoin PRs modifying the bitcoin crate label Jun 24, 2024
@apoelstra
Copy link
Copy Markdown
Member

This looks great to me. A minor point on naming is that I notice we have from_*_idx methods, which are weird because (a) idx is an abbreviation that we don't use in other method names (according to our API docs it does appear in the bip152 module which we should also fix), but (b) our types are called Numbers not indexes.

I think we should change the names of the new types to HardenedChildNumber etc rather than just HardenedChild. This is a mouthful but I think usually users won't be using these, since in almost all cases they'd want the more-generic-and-shorter ChildNumber enum. And we should rename from_hardened_idx to from_hardened_child_number and same for the non-hardened one.

@storopoli
Copy link
Copy Markdown
Contributor Author

This looks great to me. A minor point on naming is that I notice we have from_*_idx methods, which are weird because (a) idx is an abbreviation that we don't use in other method names (according to our API docs it does appear in the bip152 module which we should also fix), but (b) our types are called Numbers not indexes.

Ok will do in this PR.

I think we should change the names of the new types to HardenedChildNumber etc rather than just HardenedChild. This is a mouthful but I think usually users won't be using these, since in almost all cases they'd want the more-generic-and-shorter ChildNumber enum.

Should we remove the pub from these new structs?

@github-actions github-actions bot added the test label Jun 24, 2024
@coveralls
Copy link
Copy Markdown

coveralls commented Jun 24, 2024

Pull Request Test Coverage Report for Build 9650838641

Details

  • 93 of 122 (76.23%) changed or added relevant lines in 2 files are covered.
  • 12 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.09%) to 83.052%

Changes Missing Coverage Covered Lines Changed/Added Lines %
bitcoin/src/psbt/mod.rs 6 9 66.67%
bitcoin/src/bip32.rs 87 113 76.99%
Files with Coverage Reduction New Missed Lines %
bitcoin/src/psbt/mod.rs 1 78.67%
bitcoin/src/bip32.rs 11 84.34%
Totals Coverage Status
Change from base Build 9639464882: -0.09%
Covered Lines: 19553
Relevant Lines: 23543

💛 - Coveralls

@coveralls
Copy link
Copy Markdown

coveralls commented Jun 24, 2024

Pull Request Test Coverage Report for Build 9650886863

Details

  • 93 of 122 (76.23%) changed or added relevant lines in 2 files are covered.
  • 12 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.09%) to 83.052%

Changes Missing Coverage Covered Lines Changed/Added Lines %
bitcoin/src/psbt/mod.rs 6 9 66.67%
bitcoin/src/bip32.rs 87 113 76.99%
Files with Coverage Reduction New Missed Lines %
bitcoin/src/psbt/mod.rs 1 78.67%
bitcoin/src/bip32.rs 11 84.34%
Totals Coverage Status
Change from base Build 9639464882: -0.09%
Covered Lines: 19553
Relevant Lines: 23543

💛 - Coveralls

@storopoli storopoli marked this pull request as ready for review June 24, 2024 19:02
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure about this.
Suggestions?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From defiinitely should not panic. Since the conversion is fallible we should implement TryFrom instead if we want a conversion function.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since this type is supposed to maintain the invariant that the top bit is not set, IMO the inner field should be private. Same for the hardened struct.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, but I needed a new() method for the structs that don't check the range of the u32 for within [0, 2^31 - 1].

https://github.com/rust-bitcoin/rust-bitcoin/blob/584ab34315054f145a8368f81bca2a5cb122639f/bitcoin/src/bip32.rs#L123-L125

So that this diff

-    let num = ChildNumber::Normal { index: 0xDEADBEEF };
+    let num = ChildNumber::Normal(NormalChildNumber::new(0xDEADBEEF));

works in https://github.com/rust-bitcoin/rust-bitcoin/blob/584ab34315054f145a8368f81bca2a5cb122639f/bitcoin/tests/serde.rs#L195-L201

Because 0xDEADBEEF > 2^31 -1 is true. 🤷🏻‍♂️

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From defiinitely should not panic. Since the conversion is fallible we should implement TryFrom instead if we want a conversion function.

@coveralls
Copy link
Copy Markdown

coveralls commented Jun 25, 2024

Pull Request Test Coverage Report for Build 9659761194

Details

  • 94 of 127 (74.02%) changed or added relevant lines in 2 files are covered.
  • 12 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.1%) to 83.039%

Changes Missing Coverage Covered Lines Changed/Added Lines %
bitcoin/src/psbt/mod.rs 6 9 66.67%
bitcoin/src/bip32.rs 88 118 74.58%
Files with Coverage Reduction New Missed Lines %
bitcoin/src/psbt/mod.rs 1 78.67%
bitcoin/src/bip32.rs 11 83.89%
Totals Coverage Status
Change from base Build 9639464882: -0.1%
Covered Lines: 19554
Relevant Lines: 23548

💛 - Coveralls

@storopoli storopoli requested a review from tcharding June 25, 2024 09:21
Copy link
Copy Markdown
Collaborator

@Kixunil Kixunil left a comment

Choose a reason for hiding this comment

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

The panics are definitely wrong and I'd like the naming to be addressed as well.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think there might be a difference between index and child number. It looks to me that child number contains the hardened flag while index doesn't. So index was probably correct here. We're also storing index in the *ChildNumber which is confusing. (Also maybe if integer validity ranges stabilize in the future we can get better optimization changing it.) I don't care to change the layout in this PR though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Cc @apoelstra, shall we revert back to from_*_idx?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh, yeah, I guess index is correct then. That is confusing.

Let's expand idx to index though.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd prefer returning a more specific error but this can be done in a followup PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unrelated but I dislike this impl existing...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not what "prevent by type system" means. There should be no panic and P needs to be AsRef<[NormalChildNumber]> instead.

Though now I'm wondering why we're not using IntoIterator<Item=(Normal)ChildNumber> instead.

Copy link
Copy Markdown
Contributor Author

@storopoli storopoli Jun 25, 2024

Choose a reason for hiding this comment

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

Yes, I agree. I am trying the AsRef<[NormalChildNumber]> route. But I didn't get the implicit conversions correct yet.
I'll try harder 😄.

I like the IntoIterator idea I'll also try that.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same as above.

Copy link
Copy Markdown
Contributor Author

@storopoli storopoli Jun 25, 2024

Choose a reason for hiding this comment

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

So ckd_pub_tweak can still fail due to https://github.com/rust-bitcoin/rust-bitcoin/blob/584ab34315054f145a8368f81bca2a5cb122639f/bitcoin/src/bip32.rs#L832-L838

hence the expect here.

Any suggestions?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it's mathematically unlikely (same as guessing a private key), but I don't remember certainly, so I'll let @apoelstra to answer this. If it's mathematically unlikely we can except but a comment about likelihood would help.

If it's not unlikely we need to use Result.

Copy link
Copy Markdown
Contributor Author

@storopoli storopoli Jun 25, 2024

Choose a reason for hiding this comment

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

Yes because you're guaranteed to have 32 bytes

https://github.com/rust-bitcoin/rust-bitcoin/blob/584ab34315054f145a8368f81bca2a5cb122639f/bitcoin/src/bip32.rs#L810-L825

and ckd_pub_tweak only fails if SecretKey::from_slice is not 32 bytes and within the secp curve order:

https://github.com/rust-bitcoin/rust-secp256k1/blob/6648126c69ce8056d053cbaa22246d12c76f3fc1/src/key.rs#L211-L232:

    /// use secp256k1::SecretKey;
    /// let sk = SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order");
    /// ```
    #[inline]
    pub fn from_slice(data: &[u8]) -> Result<SecretKey, Error> {
        match <[u8; constants::SECRET_KEY_SIZE]>::try_from(data) {
            Ok(data) => {
                unsafe {
                    if ffi::secp256k1_ec_seckey_verify(
                        ffi::secp256k1_context_no_precomp,
                        data.as_c_ptr(),
                    ) == 0
                    {
                        return Err(InvalidSecretKey);
                    }
                }
                Ok(SecretKey(data))
            }
            Err(_) => Err(InvalidSecretKey),
        }
    }

That's why I've put the expect(), this will not fail. I could use unwrap() but that's not recommended inside src/ code (it's fine for examples and tests though).

Copy link
Copy Markdown
Collaborator

@Kixunil Kixunil Jun 25, 2024

Choose a reason for hiding this comment

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

I mean not on the curve - essentially someone managing to create a derivation number that tweaks by the inverse of the key. I can't remember if there's hashing or not. If there is it should be impossible. LOL, it's literally in the code you posted - there's HMAC. So it's impossible to trigger it intentionally, therefore the except is valid. I'd still appreciate a cryptographer double checking my claim though. :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yep, you're correct on the crypto front.

But why does ckd_pub_tweak return a Result if it can't fail. I think any expects should appear next to the HMAC. (If ckd_pub_tweak is returning a result because it takes a general child number, then it needs to be inverted with ckd_pub -- the fallible methods should call the infallible ones,not the other way around.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah, ckd_pub_tweak should just accept NormalChildNumber and not return Result.

@storopoli storopoli marked this pull request as draft June 25, 2024 14:29
@storopoli
Copy link
Copy Markdown
Contributor Author

Note to myself (TODO)

Ok, we can now accept in Xpub::derive_pub a AsRef<[NormalChildNumber]>> but we need to implement the AsRef<[NormalChildNumber]>> for DerivationPath

@coveralls
Copy link
Copy Markdown

coveralls commented Jun 25, 2024

Pull Request Test Coverage Report for Build 9664378557

Details

  • 84 of 114 (73.68%) changed or added relevant lines in 2 files are covered.
  • 12 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.1%) to 83.055%

Changes Missing Coverage Covered Lines Changed/Added Lines %
bitcoin/src/psbt/mod.rs 6 9 66.67%
bitcoin/src/bip32.rs 78 105 74.29%
Files with Coverage Reduction New Missed Lines %
bitcoin/src/psbt/mod.rs 1 78.67%
bitcoin/src/bip32.rs 11 84.08%
Totals Coverage Status
Change from base Build 9663915609: -0.1%
Covered Lines: 19576
Relevant Lines: 23570

💛 - Coveralls

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Jun 25, 2024

we need to implement the AsRef<[NormalChildNumber]>> for DerivationPath

That is impossible. At best we could completely revamp the layout to allow unsafe casts but that loses conversions to enum. Now I realize the situation is pretty annoying and maybe the extension traits for Vec<*ChildNumber> & co are the right approach, so people could enforce normal numbers at parsing time but that still may suck. I think that a fallible version would still be useful.

(BTW time locks kinda have the same problem but people don't store them in arrays, so it's fine.)

@jaoleal
Copy link
Copy Markdown
Contributor

jaoleal commented Jun 25, 2024

I did a PR right now to @storopoli s branch...
where we did a tryfrom function that, from a DerivationPath, we could extract a vec<NormalChildNumber> that made the changes to work properly...
Still not resolving everything but we got progress.

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Jun 25, 2024

@jaoleal that can be useful in a bunch of cases but it allocates so it'd be nice to see a non-allocating version too.

@jaoleal
Copy link
Copy Markdown
Contributor

jaoleal commented Jun 25, 2024

I`m not certain of what kind of "allocation" do you mean.

I wonder if is because of the
let mut ret = Vec::new()

Its possible to exclude that and retrieve directly from the DerivationPath struct. the only restriction is that we need to return a vec<KindOfKey> instead of a direct slicing...
But not possible for this one to be infallible because of the unreachable possibility of containing a HardenedChildNumber

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Jun 25, 2024

Pedantically, it's not Vec::new but it's close enough. Returning a new vec requires an allocation (and the fact you mentioned new suggests your conversion isn't even optimal).

Its possible to exclude that and retrieve directly from the DerivationPath struct. the only restriction is that we need to return a vec<KindOfKey> instead of a direct slicing...

I don't understand this but it's getting late here, so maybe I'm just tired.

But not possible for this one to be infallible

Exactly that's why a fallible alternative method is useful.

@storopoli
Copy link
Copy Markdown
Contributor Author

That is impossible. At best we could completely revamp the layout to allow unsafe casts but that loses conversions to enum. Now I realize the situation is pretty annoying and maybe the extension traits for Vec<*ChildNumber> & co are the right approach, so people could enforce normal numbers at parsing time but that still may suck. I think that a fallible version would still be useful.

Yes, exactly. If we've Xpub::derive_pub only accepting AsRef<[NormalChildNumber]>> we then break the Xpub::derive_pub API and we need to fix all examples and other code to use NormalChildNumber.
This is a significant departure from the current API and also it would make Xpub::derive_pub very different from siblings that take a DerivationPath.

We can't infallible convert DerivationPath into a [NormalChildNumber] because nothing guarantees that the derivation path only contains NormalChildNumber (can still have HardenedChildNumber).

Is this a good tradeoff? Make Xpub::derive_pub but breaking the API?
If yes, then I will finish the work in this PR.
Otherwise we can close.

@coveralls
Copy link
Copy Markdown

coveralls commented Jun 26, 2024

Pull Request Test Coverage Report for Build 9678710862

Details

  • 94 of 126 (74.6%) changed or added relevant lines in 2 files are covered.
  • 11 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.1%) to 83.058%

Changes Missing Coverage Covered Lines Changed/Added Lines %
bitcoin/src/psbt/mod.rs 6 9 66.67%
bitcoin/src/bip32.rs 88 117 75.21%
Files with Coverage Reduction New Missed Lines %
bitcoin/src/psbt/mod.rs 1 78.67%
bitcoin/src/bip32.rs 10 84.08%
Totals Coverage Status
Change from base Build 9667039141: -0.1%
Covered Lines: 19581
Relevant Lines: 23575

💛 - Coveralls

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Jun 26, 2024

@storopoli API breaks are expected these days and we prefer better API over unbroken API.

I personally believe we should have both methods because some applications will prefer to enforce normal path at parsing and the conversion is complicated. Something like this should work:

// note the lack of pub
fn internal_derive_pub<T: TryInto<NormalChildNumber>(&self, path: impl Iterator<Item=T>) -> Result<Xpub, T::Error> {
// current code here, call .try_into()? on each item in the for loop
}

pub fn derive_pub<I: IntoIterator<Item=NormalChildNumber>>(&self, path: I) -> Xpub {
    self.internal_derive_pub(path.into_iter()).unwrap_or_else(|never| match never {})
}

pub fn try_derive_pub<I: IntoIterator<Item=ChildNumber>>(&self, path: I) -> Result<Xpub, NormalChildNumberError> {
    self.internal_derive_pub(path.into_iter())
}

(We could also just expose the internal method and let people use unwrap_or_else but I think we should make the better approach less annoying.)

@jaoleal
Copy link
Copy Markdown
Contributor

jaoleal commented Jun 26, 2024

pub fn derive_pub<I: IntoIterator<Item=NormalChildNumber>>(&self, path: I) -> Xpub {
    self.internal_derive_pub(path.into_iter()).unwrap_or_else(|never| match never {})
}

What is the point of using a non-null safe language to do this ?

Including the

impl TryFrom<DerivationPath> for Vec<NormalChildNumber> {
    type Error = Error;
    fn try_from(value: DerivationPath) -> Result<Self, Self::Error> {
        let mut ret = Vec::new();
        for cnum in value.0.iter() {
            match *cnum {
                ChildNumber::Normal(normal) => {
                    ret.push(normal);
                },
                _ => return Err(Error::InvalidChildNumberFormat),
            }
        }
        Ok(ret)
    }
}

we could improve your proposal of a function for each possibility. Like:

// Certain of normalized path
fn derive_pub_pure( v: Vec<NormalChildNumber>) -> Xpub {}

// Uncertain about whats inside
fn try_derive_pub( v: DerivationPath) -> Result<Xpub, Error>{}

And use it like

let path: Vec<NormalChildNumber> = DerivationPath::from(*).try_into()

derive_pub_pure(path)


///Would work here too
try_derive_pub(path)

///Since a vector of Normals are easily convertible for DerivationPath

Pedantically, it's not Vec::new but it's close enough. Returning a new vec requires an allocation (and the fact you mentioned new suggests your conversion isn't even optimal).

Improving is not hard, we can iterate DerivationPath and return a owned vector slice...

@apoelstra
Copy link
Copy Markdown
Member

Is this a good tradeoff? Make Xpub::derive_pub but breaking the API?

Yes. Don't worry about DerivationPath. Maybe we need to make a second NonHardenedDerivationPath or something to handle the case where users want an entire path of non-hardened child numbers, but we can do that later in a non-breaking way.

Right now we have a derive_pub method which unambiguously returns errors that can be caught at compile-time because it is doing an internal type coversion that users could be doing themselves (and which may not even be necessary).

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Jun 26, 2024

What is the point of using a non-null safe language to do this ?

I don't understand what relation this has to non-null. Care to explain?

we could improve your proposal of a function for each possibility. Like:

Sorry, now I see I messed up the types. Check the fixed version.

And use it like

In your example you're doing an unneeded heap allocation. Let's not do those.

Improving is not hard, we can iterate DerivationPath and return a owned vector slice...

I don't understand, returning an owned vec requires heap allocation.

@jaoleal
Copy link
Copy Markdown
Contributor

jaoleal commented Jun 26, 2024

In your example you're doing an unneeded heap allocation. Let's not do those.

The only way that i could think of have certainty of ChildNumbers types and not using a heap allocation(I think, not expert on memory allocation) is:

//Here we just get rid of some abstraction since `NormalChildNumber` and `HardenedChildNumber`
//implement the same traits.
pub enum ChildNumber {
    /// Non-hardened key
    NormalChildNumber(u32),
    /// Hardened key
    HardenedChildNumber(u32),
}

struct DerivationPath<ChildNumber> (Vec<ChildNumber>);

impl DerivationPath<ChildNumber>{
    fn to_normal( path: Self) -> Result<DerivationPath<NormalChildNumber>, Error> {
        path.iter().for_each(|cnum| {
            if cnum < 2147483647 {
                cnum = ChildNumber::NormalChildNumber(cnum);
            } else {
                return Error
        }
        });
        Self
    }
    fn to_hard( path: Self) -> Result<DerivationPath<HardenedChildNumber>, Error> {
        path.iter().for_each(|cnum| {
            if cnum > 2147483647 {
                cnum = ChildNumber::HardenedChildNumber(cnum);
            } else {
                return Error
        }
        });
        Self
    }
}
//No more uncertainty
fn derive_pub(p: DerivationPath<NormalChildNumber>) -> Xpub {}

Maybe the 2147483647 is wrong, but is just a poc.

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Jun 26, 2024

@jaoleal that doesn't work and there's no need to do that. The code that's written right now already avoids allocation. We only need to refactor it a bit to make fallibility generic.

@storopoli
Copy link
Copy Markdown
Contributor Author

I've spoken offline with @jaoleal and he will be taking over this PR.
We need more developers to rust-bitcoin.
I'll gladly review the PR and, if necessary, take over future PRs.

@storopoli storopoli closed this Jun 28, 2024
@jaoleal
Copy link
Copy Markdown
Contributor

jaoleal commented Jun 28, 2024

I`ll reopen the PR tagging the issue when i think better of the @Kixunil Iterator's strategy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-bitcoin PRs modifying the bitcoin crate test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

derive_xpriv should not return a Result

6 participants