Skip to content

Fix: derive_pub() is now infalible#3036

Closed
jaoleal wants to merge 1 commit intorust-bitcoin:masterfrom
jaoleal:InfalibleDerivePub
Closed

Fix: derive_pub() is now infalible#3036
jaoleal wants to merge 1 commit intorust-bitcoin:masterfrom
jaoleal:InfalibleDerivePub

Conversation

@jaoleal
Copy link
Copy Markdown
Contributor

@jaoleal jaoleal commented Jul 13, 2024

Rest of #2911.

Agreeing with storopoli 's comment and tried to follow Kixunil advice.

The changes hopefully will confirm that derive_pub() never fails.
The NormalChildIterator ensures that every ChildNumber passed to internal_derive_pub() is a ChildNumber::Normal, in other case the iterator will end when find a ChildNumber::Hardened. This can introduce to a new behavior where we try to insert a DerivationPath that contain a ChildNumber::Hardened and derive_pub() will not fail but instead, yielding a different public key.

The focus was not changing too much the code and make something that serves as a safe filter for derive_pub(), because of this, NormalChildIterator is meant to only be used inside derive_pub()

Sorry for taking so long to become with a solution.

@github-actions github-actions bot added the C-bitcoin PRs modifying the bitcoin crate label Jul 13, 2024
@coveralls
Copy link
Copy Markdown

coveralls commented Jul 13, 2024

Pull Request Test Coverage Report for Build 10483091871

Details

  • 198 of 327 (60.55%) changed or added relevant lines in 4 files are covered.
  • 15 unchanged lines in 1 file lost coverage.
  • Overall coverage decreased (-0.3%) to 82.498%

Changes Missing Coverage Covered Lines Changed/Added Lines %
bitcoin/src/psbt/mod.rs 8 13 61.54%
bitcoin/src/bip32.rs 186 310 60.0%
Files with Coverage Reduction New Missed Lines %
bitcoin/src/bip32.rs 15 76.3%
Totals Coverage Status
Change from base Build 10475541682: -0.3%
Covered Lines: 19694
Relevant Lines: 23872

💛 - Coveralls

@tcharding
Copy link
Copy Markdown
Member

tcharding commented Jul 14, 2024

OT I removed the mentions from the PR description because when used in the description box they cause github to spam the person with notifications.

cc @storopoli and @Kixunil (because this is not my PR and I edited the description).

@github-actions github-actions bot added the test label Jul 15, 2024
Copy link
Copy Markdown
Contributor Author

@jaoleal jaoleal left a comment

Choose a reason for hiding this comment

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

Addressing some requested changes by @Kixunil

@jaoleal jaoleal requested review from Kixunil and storopoli July 15, 2024 21:14
@jaoleal jaoleal requested a review from Kixunil July 18, 2024 03:06
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.

Looks much better but there are still some blocking issues.

@jaoleal jaoleal requested a review from Kixunil July 19, 2024 01:58
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.

Damn, my review didn't get sent again. I'm starting to suspect network issues.

@jaoleal
Copy link
Copy Markdown
Contributor Author

jaoleal commented Jul 20, 2024

So, to get the things done to merge, ill consider the marjority of the "stable"(we have an idea to follow), and implement it on the examples and the rest of the code(saw some things while running 'just check'). After that, squash.

@storopoli
Copy link
Copy Markdown
Contributor

To get a proper final review you need

  1. get CI passing by either checking the CI job lint (https://github.com/rust-bitcoin/rust-bitcoin/actions/runs/9997644130/job/27634689315) or calling just lint
  2. Consider squashing all your commits into a single commit, or if not the case reword your commits to something more informative. Ideally they should be atomic in the sense that cherry-picking any of them you would get CI passing (that's why I think squashing everything makes more sense here)

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Jul 20, 2024

Also regarding squashing, not all commits deserve to be squashed but I think you'll have to squash these. Or edit the git history to e.g. create the newtypes first.

@jaoleal jaoleal force-pushed the InfalibleDerivePub branch 2 times, most recently from 33f400e to d9f7f8d Compare July 22, 2024 16:26
Copy link
Copy Markdown
Contributor

@storopoli storopoli left a comment

Choose a reason for hiding this comment

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

Bip32 Remake Part 2 (Directors Cut) is not a good commit message.
What about:

bip32: infallible derive_pub

?

@jaoleal
Copy link
Copy Markdown
Contributor Author

jaoleal commented Jul 22, 2024

Bip32 Remake Part 2 (Directors Cut) is not a good commit message. What about:

bip32: infallible derive_pub

?

I know 🤣.
This is a placeholder.
Actually wanted a name that yields the idea of the actual change, i think that Infallible derive_pub does not represents all the changes.
Was about to comment about it, but I'm actually struggling with the CI.

@jaoleal jaoleal force-pushed the InfalibleDerivePub branch 2 times, most recently from 47f52af to f2ea8c7 Compare July 22, 2024 18:50
@github-actions
Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

@github-actions github-actions bot added the API break This PR requires a version bump for the next release label Jul 22, 2024
@jaoleal jaoleal requested a review from Kixunil July 22, 2024 21:13
@jaoleal jaoleal mentioned this pull request Aug 13, 2024
Copy link
Copy Markdown
Contributor

@storopoli storopoli left a comment

Choose a reason for hiding this comment

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

A couple more.
Please note that there are still old comments/suggestion that were not addressed by the new commit.
GitHub might have hidden it away in the "view more" style of buttons.

And please do not make dozens of commits. The bot is very "trigger-happy".

Thanks for working on this!

@github-actions
Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

@jaoleal jaoleal force-pushed the InfalibleDerivePub branch from cbb1f47 to c3c3c35 Compare August 13, 2024 17:38
@jaoleal jaoleal requested a review from Kixunil August 13, 2024 17:41
@github-actions
Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

Copy link
Copy Markdown
Contributor

@storopoli storopoli left a comment

Choose a reason for hiding this comment

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

ACK c3c3c3553f4dc2cb02849379faabcdc04d1cf017

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.

I cannot finish the review due to logistical reasons. I'll do it later.

The commit still doesn't have my commit message and @jaoleal please stop clicking "Resolve" on conversations that are not really resolved.

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 example is silly, rather than parsing it should just construct the path directly. But not needed in this 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.

This conversion shouldn't be needed. If it is, we have a bug in the API.

Copy link
Copy Markdown
Contributor Author

@jaoleal jaoleal Aug 16, 2024

Choose a reason for hiding this comment

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

indeed
I added I: Into<ChildNumber>, and worked fine without the method

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.

Shouldn't be needed.

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.

In this case i dont see a good fix... KeySource is a tupple spread all of the code... trying any abstraction or change on it will run out of scope for this 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.

I mean the conversion.

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'll hack deeper for that

Copy link
Copy Markdown
Contributor Author

@jaoleal jaoleal Aug 16, 2024

Choose a reason for hiding this comment

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

/// Full information on the used extended public key: fingerprint of the
/// master extended public key and a derivation path from it.
pub type KeySource = (Fingerprint, DerivationPath);

We could make KeySource a newtype, but in another pr... i dont see how to discard the conversion without changing the KeySource itself.

Ill gladly work on it after this one. by now, i would leave the example as it is.

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.

Oh, I think I understand now. OK, fine to look at it later. It may be that we can't actually change it because it might break the spec but we'd have to dig much deeper.

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 should be forwarded using fmt::Display::fmt(&self.to_raw_number, f) so that all formatting works as with integers.

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.

"resolved"

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.

Needs forwarding.

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.

"resolved"

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 should be pub, the allow is wrong.

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.

"resolved"

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 don't love the name but I can't think of any better. But why not just have fn push_child(&mut self, cn: ChildNumber)?

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 did not understand the point of this comment, do you want a rename ?

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 suggest changing both the interface and the name.

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.

What's the point of this if there's already extend?

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.

abstraction before calling extend. redundant but i would prefer to maintain this function rather than deleting it and further rewriting it back because i dont want thousand impls on the code

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.

Why can't you just replace extend_from calls with extend?

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.

ok

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 don't see the point of creating a new one, especially since you're not optimizing allocations. Further I think the method should be called join like on Path.

@jaoleal
Copy link
Copy Markdown
Contributor Author

jaoleal commented Aug 16, 2024

The commit still doesn't have my commit message

For sure i didnt pasted literally your commit message, but the message is literally a bullet point of yours.

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Aug 16, 2024

If I spent all that time writing a thoughtful message I'd prefer it to be there. Why wouldn't you just copy-paste it?

@jaoleal
Copy link
Copy Markdown
Contributor Author

jaoleal commented Aug 16, 2024

Why wouldn't you just copy-paste it?

It's big but okay

@jaoleal jaoleal force-pushed the InfalibleDerivePub branch from c3c3c35 to 19c175f Compare August 19, 2024 12:33
@jaoleal jaoleal requested a review from Kixunil August 19, 2024 12:34
@github-actions
Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

I have not been following this work closely so please forgive me if I suggest something already discussed. Also I note that the bip32 module is still a mess but this PR is a definite improvement. Thanks for contributing.

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.

This change shouldn't be in here, its unrelated.

Copy link
Copy Markdown
Contributor Author

@jaoleal jaoleal Aug 21, 2024

Choose a reason for hiding this comment

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

wtf

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.

Maybe was something that i called like fmt or something like that. Ill put the blank line back i guess.

Comment on lines 271 to 270
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.

nit: Perhaps just use expect here like done in other places.

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.

Showing the proper error handling to users might be better.

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.

Fair point.

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.

Suggested change
/// Sub-module for [`HardenedChildNumber`] type
/// This module exists to protect the invariants on the inner field of [`HardenedChildNumber`].

Comment on lines 137 to 138
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.

Suggested change
/// A child number for a derived key
/// Indexes `2_147_483_648` to `4_294_967_295` (the second half of all possible children)
/// A child number for a derived key.
///
/// Guarantees that the inner field is a valid hardened value (indexes `2_147_483_648` to
/// `4_294_967_295`, the second half of all possible children).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Won't these auto doc wrapping formatting be done in the weekly rustfmt PR job?

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.

rustfmt doesn't change rustdocs line lengths.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh I swear that this was set to true

format_code_in_doc_comments = false

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.

I think its correct to be false, its kind of nice to control manually docs IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes you are right. Sometimes that can be really nasty...

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.

All the public re-exports would fit in with the style we use better if they were put below the 'use crate::foo' statements and written like this:

#[rustfmt::skip]                // Keep public re-exports separate.
#[doc(inline)]
pub use self::{
    hardened::HardenedChildNumber,
    normal::NormalChildNumber,
    derivation_path::{
        DerivationPath, IntoIter as DerivationPathIntoIter, Iter as DerivationPathIter,
    },
    normal_derivation_path::{
        NormalDerivationPath, IntoIter as NormalDerivationPathIntoIter, Iter as NormalDerivationPathIter,
    },
};

Feel free not to use my custom formating, I grouped similar things instead of using alphabetic order, also one line is longer that the formatter allows.

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.

EDIT: Note below I question the non-uniformity of the NormalDerivationPath and DerivationPath types, same question applies to the module names.

Copy link
Copy Markdown
Contributor Author

@jaoleal jaoleal Aug 21, 2024

Choose a reason for hiding this comment

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

EDIT: Note below I question the non-uniformity of the NormalDerivationPath and DerivationPath types, same question applies to the module names.

You say... the names ?

Feel free not to use my custom formating, I grouped similar things instead of using alphabetic order, also one line is longer that the formatter allows.

I dont spend to much time organizing this normally because i know that, usually on many other projects, the fmt will just mess it up or standardize it.
But, nice! its better !

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.

Perhaps this should be called from_idx to match the other two constructors? (And put above from_normal_idx.)

But really, this idx stuff is non-uniform, there are other functions called from_index - we should pick one. Can be done as a follow up 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.

from_raw_index rather.

I prefer index rather than idx.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I prefer index as well

Comment on lines 288 to 296
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.

All this could be written as

    pub fn from_idx(index: u32) -> Self {
        match NormalChildNumber::from_index(index) {
            Ok(normal) => ChildNumber::Normal(normal),
            Err(_) => ChildNumber::Hardened(HardenedChildNumber::from_index(index).expect("valid since not normal")),
        }
    }

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.

Perhaps this should be HardenedDerivationPath to match the NormalDerivationPath?

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.

No, it's not hardened. It can be anything.

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.

My bad.

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.

nit: Should probably be P to match the other one in derive_priv

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.

Same suggestions as for hardened module.

@jaoleal jaoleal force-pushed the InfalibleDerivePub branch from 19c175f to f3cf8dd Compare August 21, 2024 03:07
@jaoleal
Copy link
Copy Markdown
Contributor Author

jaoleal commented Aug 21, 2024

Adressed the requested changes by @tcharding

@jaoleal jaoleal requested a review from tcharding August 21, 2024 03:08
@github-actions
Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

The BIP32 API had several problems that we wanted to fix for some time.
It didn't protect the invariants of child numbers, had some
methods fallible even if the errors were cryptographically
impossible, it lacked infallible APIs for deriving xpubs when
absence of hardened children was statically known, and had
annoying ChildNumber enum with named field for no good reason.

In this commit we do a bunch of refactoring to fix these problems.
We add newtypes for the child numbers and a newtype for derivation
path that is guaranteed to only contain normal child numbers with
appropriate parsing and conversion methods. We refactor the
ChildNumber enum to use tuple variants containing the newtypes
and fix the infallible APIs to not return Result. Dealing with
hardened child numbers can still be annoying or non-performant
in some scenarios so we still provide a fallible derivation method.
Further we put the newtypes into separate submodules to make
accidental breaking of invariatns less likely.

Signed-off-by: jaoleal <jgleal@protonmail.com>
Signed-off-by: jaoleal <=>
@jaoleal jaoleal force-pushed the InfalibleDerivePub branch from f3cf8dd to c3e0813 Compare August 21, 2024 03:37
@github-actions
Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

@tcharding
Copy link
Copy Markdown
Member

tcharding commented Aug 21, 2024

I'm not sure what to do with this PR. Its 700 lines of changes, its very hard to review just looking at the diff but when I look at the bip32 module there are so many things that need improving its hard to know where to start.

My primary concern is that at the moment it is not obvious what an "index" is and what a "number" is.

In order to fully understand the problem I had a play, would you be open to incorporating some additional ideas? You could do any of the following (if you are willing)

  1. Tell me to go away, and request this PR is merged as is
  2. Re-work this PR into a series of discreet patches that gets us to a place where the hardened / normal modules look something like what is posted below
/// A child key number.
///
/// Please note, "index" and "number" have specific defined meanings in this module.
///
/// - A "number" is always less that 2^31 and is used in derivation paths eg "0'/0/1".
/// - An "index" is the encoded form, where one bit is used to represent normal vs hardened.
///
/// > Each extended key has 231 normal child keys, and 231 hardened child keys. Each of these
/// > child keys has an index. The normal child keys use indices 0 through 231-1. The hardened
/// > child keys use indices 231 through 232-1. To ease notation for hardened key indices, a
/// > number iH represents i+231.
///
/// So, when serializing a child number as an index BIP-32 uses the top bit of the index (greater
/// than or equal to 2^31) to indicate hardened child keys.
#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)]
pub enum ChildNumber {
    /// A normal child number e.g., non-hardened.
    Normal(NormalChildNumber),
    /// A hardened child number.
    Hardened(HardenedChildNumber),
}

/// This module exists to protect the invariants on the inner field of [`HardenedChildNumber`].
mod hardened {
    use super::*;

    /// A hardened child number for a derived key.
    #[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)]
    pub struct HardenedChildNumber(u32);

    impl HardenedChildNumber {
        /// The first hardened child (number 0).
        pub const ZERO: Self = HardenedChildNumber(0);

        /// The second hardened child (number 1).
        pub const ONE: Self = HardenedChildNumber(1);

        /// Creates a [`HardenedChildNumber`] from a `number` (less than 2^31).
        ///
        /// # Errors
        ///
        /// If the top bit of `number` is set.
        pub const fn from_number(number: u32) -> Result<HardenedChildNumber, NumberError> {
            if index & (1 << 31) == 0 {
                Ok(HardenedChildNumber(index))
            } else {
                Err(NumberError(index))
            }
        }

        /// Creates a hardened child number from an `index` (greater than 2^31).
        ///
        /// # Errors
        ///
        /// If the top bit of `index` is not set.
        pub const fn from_index(index: u32) -> Result<HardenedChildNumber, HardenedIndexError> {
            if index & (1 << 31) != 0 {
                Ok(HardenedChildNumber(index))
            } else {
                Err(HardenedIndexError(index))
            }
        }

        /// Creates a hardened child number from a [`ChildNumber`].
        pub const fn from_child_number(number: ChildNumber) -> Result<HardenedChildNumber, HardenedChildNumberError> {
            match number {
                ChildNumber::Hardened(num) => Ok(num),
                ChildNumber::Normal(num) => Err(HardenedChildNumberError(number)),
            }
        }

        /// Converts this hardened child number to a number (less than 2^31).
        pub const fn to_number(self) -> u32 { self.0 }

        /// Converts this hardened child number to an index (greater than 2^31).
        pub const fn to_index(self) -> u32 { self.0 & (1 << 31) }

        /// Converts this hardened child number to a [`ChildNumber`].
        pub const fn to_child_number(self) -> ChildNumber { ChildNumber::Hardened(self) }
    }
}

/// This module exists to protect the invariants on the inner field of [`NormalChildNumber`].
mod normal {
    use super::*;

    /// A normal child number for a derived key.
    ///
    /// Guaranteed to be less than 2^31.
    #[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)]
    pub struct NormalChildNumber(u32);

    impl NormalChildNumber {
        /// The first normal child (number 0).
        pub const ZERO: Self = NormalChildNumber(0);

        /// The second normal child (number 1).
        pub const ONE: Self = NormalChildNumber(1);

        /// Creates a normal child number from a number (less than 2^31).
        ///
        /// # Errors
        ///
        /// If the top bit of `number` is set.
        pub const fn from_number(index: u32) -> Result<NormalChildNumber, NumberError> {
            if index & (1 << 31) == 0 {
                Ok(NormalChildNumber(index))
            } else {
                Err(NumberError(index))
            }
        }

        /// Creates a normal child number from an index (less than 2^31).
        ///
        /// # Errors
        ///
        /// If the top bit of `index` is set.
        pub const fn from_index(index: u32) -> Result<NormalChildNumber, NormalIndexError> {
            if index & (1 << 31) == 0 {
                Ok(NormalChildNumber(index))
            } else {
                Err(NormalIndexError(index))
            }
        }

        /// Creates a normal child number from a [`ChildNumber`].
        pub const fn from_child_number(number: ChildNumber) -> Result<NormalChildNumber, NormalChildNumberError> {
            match number {
                ChildNumber::Normal(num) => Ok(num),
                ChildNumber::Hardened(_) => Err(NormalChildNumberError(number)),
            }
        }

        /// Converts this normal child number to a number (less than 2^31).
        pub const fn to_number(self) -> u32 { self.0 }

        /// Converts this normal child number to an index (less than 2^31).
        pub const fn to_index(self) -> u32 { self.to_number() }

        /// Converts this normal child number to a [`ChildNumber`].
        pub const fn to_child_number(self) -> ChildNumber { ChildNumber::Normal(self) }
    }
}

impl fmt::Display for HardenedChildNumber {
    /// Displays the number without the ' or h marker.
    ///
    /// The displayed number is always less than 2^31.
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&self.to_number(), f)
    }
}

impl fmt::Display for NormalChildNumber {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&self.to_number(), f)
    }
}

impl FromStr for HardenedChildNumber {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Error> {
        let number = s.parse::<u32>()?;
        HardenedChildNumber::from_number(index)
    }
}

impl FromStr for NormalChildNumber {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Error> {
        let number = s.parse::<u32>()?;
        NormalChildNumber::from_number(number)
    }
}

impl From<NormalChildNumber> for ChildNumber {
    fn from(num: NormalChildNumber) -> Self { ChildNumber::Normal(num) }
}

impl From<HardenedChildNumber> for ChildNumber {
    fn from(num: HardenedChildNumber) -> Self { ChildNumber::Hardened(num) }
}

impl TryFrom<ChildNumber> for NormalChildNumber {
    type Err = NormalChildNumberError;

    fn from(num: NormalChildNumber) -> Self { Self::from_child_number(number) }
}

impl TryFrom<ChildNumber> for HardenedChildNumber {
    type Err = HardenedChildNumberError;

    fn from(num: HardenedChildNumber) -> Self { Self::from_child_number(number) }
}

Still TODO

  • Create a whole bunch of new error types
  • Go over the ChildNumber (and rest of module) to make sure the index/number nomenclature is correctly followed.

This is a non-trivial amount of work, I appreciate you taking it on, if you choose to do so. If you don't I can get to it but it is waaay down my list of priorities.

@jaoleal
Copy link
Copy Markdown
Contributor Author

jaoleal commented Aug 22, 2024

In order to fully understand the problem I had a play, would you be open to incorporating some additional ideas?

Yes, of course... I just wonder if is this being caused by my lack of knowledge in rust/bitcoin or because i am not used to the design of rust-bitcoin itself. I am sorry that this PR took so long and still don't make some expectations.
But will be a trade-off because i have some other works to do too.

Working on anything here is also working on any other thing outside but related to bitcoin programming in rust. I'm eagerly to help and will stick to apoelstra advice that he gave on some podcast talking about this specific issue.

I will review every line of this PR but this may took another couple of days to be done(i need to focus a little bit on my internship and getting a real job for myself). Thank you for the time that yall spent giving good advices and hope to help more after this one.

@apoelstra
Copy link
Copy Markdown
Member

apoelstra commented Aug 22, 2024

For my part I've only been skimming this PR and it's got 400 comments and (as Tobin says) 700 lines of changes in one commit. So I'm skeptical that we'll be able to get this in as-is.

@jaoleal I would suggest that you try to find individual parts (say, renaming stuff to use "index" and "number" consistently) and PR those separately. This will be tough to do because of how tightly-coupled everything is. Another strategy might be to make a new bip32_alt module, write everything piecemeal from scratch, then in the final commit rename it to bip32 and delete the old module.

And I understand that you're busy and have higher-priority things to do (focusing on your career certainly sounds more important than banging your head against a volunteer contribution to this project!) so possibly one of us will need to do this. Or maybe out of respect for your time we'll need to merge the PR in an incomplete state and one of us should try to make a followup. Unsure.

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Aug 23, 2024

@tcharding to prove that index vs number is confusing you've swapped their meanings in your example documentation. ;) (But I personally find the names intuitive once I understood the difference.)

I personally think the main reasons for the complexity of the PR are:

  • We didn't know how complicated preventing of panics would be
  • When writing it was clear that we're hitting the previous design problems around numbers and went to fix them too
  • git has no idea what we did with the code and is showing it as garbage

I think separating number change from path change should resolve most of the complexity. @jaoleal if you feel like it you can try but if you don't, I'll take a look at some point soon enough to hit the next release because I happen to need this change. :)

@tcharding
Copy link
Copy Markdown
Member

@tcharding to prove that index vs number is confusing you've swapped their meanings in your example documentation.

Ha! Nice. I rest my case :)

@jaoleal
Copy link
Copy Markdown
Contributor Author

jaoleal commented Sep 24, 2024

Guys, quick update:

I've been working on this and staring at the PR like a dog staring at a chicken rotisserie, trying to figure out a better way to implement it. I even found a few other things that could be adjusted to fit the original idea we discussed.

I'll close this PR because it's gone beyond the initial scope. All the changes from this PR will be included in a new one. I understand this isn't ideal, and someone with more experience could probably refactor what I did, but I ask for a bit of patience. I'm not far from pushing the new changes.

I would suggest that you try to find individual parts (like renaming things to use "index" and "number" consistently) and PR those separately.

I agree that splitting things up would be better, and I’ll handle that in separate commits, making sure everything is well documented and explained.

That said, the components in this module are REALLY tightly coupled, and splitting them into separate PRs might make things worse, in my opinion.

@jaoleal jaoleal closed this Sep 24, 2024
@jaoleal jaoleal mentioned this pull request Oct 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API break This PR requires a version bump for the next release C-bitcoin PRs modifying the bitcoin crate test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants