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

Adding path derivation #538

Merged
merged 7 commits into from
Feb 14, 2016
Merged

Adding path derivation #538

merged 7 commits into from
Feb 14, 2016

Conversation

karelbilek
Copy link
Contributor

Now this might be a bit controversial, but I found something like that to be really useful.

What will be controversial, I think:

  • the usage of strings as indexes
    • yes, it's not ideal, but adding an apostrophe is a way to easily say "this is hardened" and this notation is common, and you cannot do that otherwise
  • the usage of descriptions as paths in the tests
    • again, not ideal, but I didn't want to repeat the description as path again if it's already there

@dcousens
Copy link
Contributor

dcousens commented Feb 6, 2016

ping @jprichardson, merge here, then extract to BIP32?

@@ -281,6 +281,24 @@ HDNode.prototype.deriveHardened = function (index) {
return this.derive(index + HDNode.HIGHEST_BIT)
}

// number + ' (e.g. 234') as a string -> hardened
HDNode.prototype.derivePath = function (path) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason why this just doesn't take a string as input?

@dcousens
Copy link
Contributor

dcousens commented Feb 6, 2016

IMHO, I think node.derivePath("m/0'/1/0") would be a better API, aka a String argument.

I don't think there is much value in adding an Array based path API. You can trivially do that using derive and forEach already.

@dcousens dcousens added this to the 2.3.0 milestone Feb 6, 2016
@dcousens dcousens self-assigned this Feb 6, 2016
@dcousens
Copy link
Contributor

dcousens commented Feb 6, 2016

We also intend to break out BIP32 to its own module, probably encompassing HDNode standalone with other utilities for making life easier when dealing with them.

See #508 (comment)

I'm not sure if adding these API's flies in the face of that or not.

@karelbilek
Copy link
Contributor Author

ad array based API:

My original thinking for using Array was "What if someone puts there an array that is not a path, or that is wrongly formatted? Do I have to trim spaces around the slashes? Does it have to start with 'm/' or not? What if the node is not a master and is a sub-node, the 'm/' doesn't make sense", and that by using Array I just go around all these problems.

Then I also thought about the function accepting both array and string, but that would be really ugly.

ad splitting:
sure, great idea.

@jprichardson
Copy link
Member

ping @jprichardson, merge here, then extract to BIP32?

@dcousens Yep, I don't see any harm in adding it at the moment. Agreed on not needing the array API.

@karelbilek
Copy link
Contributor Author

how exactly would you do the string-based API? Should it start with 'm/', or should it be "relative"? So for example,master.derivePath("0'/1/0'/1")(which would equal to master.derivePath("0'/1").derivePath("0'/1"))

@dcousens
Copy link
Contributor

dcousens commented Feb 6, 2016

I think 0'/1/0 is a suitable enough API.
We could easily regex, optionally skipping any prefixes until the first number is reached?

@karelbilek
Copy link
Contributor Author

By "we", you mean the code in the library, or the user of the API?

@dcousens
Copy link
Contributor

dcousens commented Feb 8, 2016

We, the developers of this library.
My suggestion was that we could make the m/ optional, but, ambiguous API's suck, so, not sure.

@karelbilek
Copy link
Contributor Author

ambiguous API's suck

Exactly. But then, I don't know how to make it unambiguous and non-sucky.

And I don't also want to totally abandon it either. 😓

@dcousens
Copy link
Contributor

dcousens commented Feb 9, 2016

@Runn1ng I think if an m/ is present, we check that !this.parentFingerprint (aka, this is a master node).
If it isn't present, proceed on with derivation.

Thoughts?

@karelbilek
Copy link
Contributor Author

Sorry for the delay. OK, that sounds reasonable

@karelbilek
Copy link
Contributor Author

Done :)

splitPath = splitPath.slice(1)
} else {
throw new Error('Not a master node')
}
Copy link
Contributor

Choose a reason for hiding this comment

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

if (this.parentFingerprint) throw new Error('Not a master node')

splitPath = splitPath.slice(1)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure standard allow if block without curly braces :) but yeah I got you. Ok, will do tomorrow

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

The argument is path of either numbers or strings.

String with "'" at the end signifies hardened path.
I am taking description field for the path.
Maybe overdoing it a bit :)
@karelbilek
Copy link
Contributor Author

I have done some cosmetic changes and rebased. The failing check has nothing to do with my code.

@dcousens
Copy link
Contributor

Sounds good, will review asap :)

@fanatid
Copy link
Member

fanatid commented Feb 13, 2016

@Runn1ng why unary plus instead praseInt?
with unary plus string / equals 0/0 because +'' gives 0, with parseInt string / throw error.

return splitPath.reduce(function (prevHd, indexStr) {
  if (indexStr.slice(-1) !== "'") return prevHd.derive(parseInt(indexStr, 10))
  return prevHd.deriveHardened(parseInt(indexStr.slice(0, -1), 10))
}, this)

@karelbilek
Copy link
Contributor Author

Good catch.

+ can cause issues - +"" is 0. parseInt("", 10) is NaN, which is better (since it causes typeforce to throw).
@karelbilek
Copy link
Contributor Author

Just thinking.... if you put in path with decimal points ("m/0.5/1") by some mistake, using "parseInt" will silently truncate it instead of throwing errors.

But I am not sure that is worth caring about.

@fanatid
Copy link
Member

fanatid commented Feb 13, 2016

As option, you can check that result of parseInt is equal original string, something llike this:

return splitPath.reduce(function (prevHd, indexStr) {
  var derive = prevHd.derive
  if (indexStr.slice(-1) === "'") {
    derive = prevHd.deriveHardened
    indexStr = indexStr.slice(0, -1)
  }

  var index = parseInt(indexStr, 10)
  if (index.toString() !== indexStr) throw new Error(...)

  return derive.call(prevHd, index)
}, this)

@karelbilek
Copy link
Contributor Author

I am also thinking if things like m/ 2 / 0' / should be allowed. parseInt will munch it just fine. (parseInt(" 3 ", 10) === 3)

I will probably do what you suggest.

Alternatively, I can just test the index with regexp if it's only numbers.

@dcousens
Copy link
Contributor

/([m]\/)?([0-9]+[']?\/)*([0-9]+[']?)/

@karelbilek
Copy link
Contributor Author

Well, I wanted to test indices one by one after splitting, which is more readable and easily debuggable, but it's true that testing the parameter at the start of a function is more in line with the rest of the code.

Again through typeforce
@karelbilek
Copy link
Contributor Author

I have added path validation to typeforce types.

@@ -26,6 +26,11 @@ function UInt53 (value) {
Math.floor(value) === value
}

function Path (value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

BIP32Path? shrug

It's private anyway, so I'm not worried too much.

Copy link
Member

Choose a reason for hiding this comment

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

+1 for BIP32Path or BIP32DerivePath

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok

Only thing I haven't been able to do is to add "undefined" to fixtures. So I could not properly test
that node.derivePath() properly fails as it should. However, I added "null" there, and "null" and "undefined"
behave in similar way in JavaScript, so that should catch that.
@karelbilek
Copy link
Contributor Author

OK, feedback was put in

@fanatid
Copy link
Member

fanatid commented Feb 14, 2016

ACK

@dcousens
Copy link
Contributor

ACK 👍

dcousens added a commit that referenced this pull request Feb 14, 2016
@dcousens dcousens merged commit 1704155 into bitcoinjs:master Feb 14, 2016
@karelbilek
Copy link
Contributor Author

😎 great

@@ -109,7 +109,7 @@
"address": "19EuDJdgfRkwCmRzbzVBHZWQG9QNWhftbZ"
},
{
"description": "m/0/2147483647",
"description": "m/0/2147483647'",
Copy link
Contributor

Choose a reason for hiding this comment

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

I might rename this to path

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would be great

@karelbilek
Copy link
Contributor Author

Thanks for merging, I kind of forgot about this PR :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants