Skip to content

Misc. improvements and cleanups #4

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

Merged
merged 11 commits into from
Mar 25, 2022
Merged

Conversation

brandonblack
Copy link

No description provided.

* Move the (much simplified) type check function to types.ts
* Use `Tapleaf` type a bit more (this might be a bad idea)
* Be more consistent in the capitalization of `Taptree`
Because the taggedHash API is typed, these are compile-time checked and
it's more clear w/o the constants.
* More clearly show the continuation and base cases in findScriptPath
* Return undefined not empty path when no path is found
  * This would lead to generating an invalid witness
* Tighten the type for HashTree to not allow 1-sided branch nodes
* Also added caching of `hashTree`, per todo.
* Added a test for this functionality
The spec uses this notation because in a spec there's no such thing as
reassigning a value. In real code it is appropriate to us accumulators
or such.
@@ -85,6 +88,12 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment {
return a.witness.slice();
});

const _hashTree = lazy.value(() => {
Copy link

Choose a reason for hiding this comment

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

👍

@@ -77,7 +77,22 @@ export interface Tapleaf {
version?: number;
}

export type Taptree = Array<[Tapleaf, Tapleaf] | Tapleaf>;
export const TAPLEAF_VERSION_MASK = 0xfe;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure if the place for this constant is in types.
Maybe it will be extracted to a BIP341 module (see bitcoinjs#1780) together with isTapleaf, isTaptree in the future. OK to be here for now.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I really didn't have a good place for it.

@motorina0
Copy link
Collaborator

Thank you for the PR!
I like simplified recursive functions for the tree parsing and the improved type checking.
I've added a few small comments.

@brandonblack
Copy link
Author

Thank you for the feedback, pushed fixes.

@brandonblack
Copy link
Author

brandonblack commented Mar 21, 2022

I pushed a large commit over here: BitGo@db3bb8e that minimizes the need to thread eccLib around. That resolves the major concern I found while reviewing the PSBT changes, although I'm still learning the PSBT code, so may have further comments in coming days.

Hopefully it goes without saying that you are welcome to cherry-pick, squash, etc. any of my commits as you see fit into your branch.

Edit to add: I suspect that we're going to want to rethink having each p2tr created verify the tweakFn, both because it's an expensive operation, and because doing that makes it difficult to use with something like a MuSig tweak function that has an internal cache that is updated by the tweak operation.

@junderw
Copy link

junderw commented Mar 21, 2022

NACK on removing eccLib from p2tr.

Perhaps it is because there is a lack of support from libraries and lots of people are "rolling their own" for now... but I have seen examples of people sending to v1 segwit programs where the 32 bytes following is not a valid point (even though it is less than P).

Bitcoin Core does not reject these, and they are essentially a black hole.

(Perhaps a standardness rule could be added to Bitcoin Core to prevent their broadcasting/propagation)

At the very least, we should require it for the address module, and anyone trying to get the address from p2tr.

But the requirement of being a valid point is especially concerning in this case since a v0 p2wsh is also 32 bytes, but it's a hash.

@junderw
Copy link

junderw commented Mar 21, 2022

If Bitcoin Core included a standardness rule, I might consider it.

@junderw
Copy link

junderw commented Mar 21, 2022

I made an issue for discussion here

@junderw
Copy link

junderw commented Mar 21, 2022

I would be comfortable removing eccLib once an official release with that standardness rule is over 60%-ish of the observable network...

@junderw
Copy link

junderw commented Mar 22, 2022

hmm... looks like consensus is opposite (standardness rule should come after every client/library under the sun has protected against it)
bitcoin/bitcoin#24106 (comment)

@brandonblack
Copy link
Author

OK, I see your point.

I think more and more that we should drop the verifyEcc stuff though. Per a side chat, we're at least marginally concerned about performance, and those are rather expensive tests to be performing on each instantiation of p2tr. bitcoinjs-lib should either provide the ECC implementation, or accept that it may be injected with one that doesn't meet the expected requirements.

Given all of this, it might make sense to apply the Factory pattern to PSBT, make an ECC library required to use PSBT in general, and avoid the conditional/optional threading. That would then open up later refactors to replace types.isPoint with a proper point check for non-Taproot as well.

@junderw
Copy link

junderw commented Mar 22, 2022

That is true. It's pretty expensive.

Perhaps we can cache eccLib...?

When comparing two objects, it returns true for equals when the object is the same reference.

We could use that and have some sort of eccLib cache that is rooted in the main library, and will use the cached library if none is passed in, and even if the same library is passed multiple times, skip the check if the reference to the library object is the same?

ie.

// eccCache.ts (do not export this in the index.ts, it should not be exposed to the public API)
export const _ECCLIB_CACHE: { eccLib?: TinySecp256k1Lib } = {};

// p2tr.ts
import { _ECCLIB_CACHE } from '../eccCache';

if (_ECCLIB_CACHE.eccLib !== eccLib) {
  check(eccLib);
  _ECCLIB_CACHE.eccLib = eccLib;
}

@motorina0
Copy link
Collaborator

I think more and more that we should drop the verifyEcc stuff though. Per a side chat, we're at least marginally concerned about performance

  • this indeed can be a performance problem. One option is to add a skipEccValidation boolean flag to PaymentOpts:
export interface PaymentOpts {
  ...
  eccLib?: TinySecp256k1Interface;
  skipEccValidation?: boolean;
}

@motorina0
Copy link
Collaborator

The caching approach would also solve the the "threading" of eccLib in PSBT.

But we can have the scenario where :

  • a p2tr(..., { eccLib: ecc }) call is made somewhere in the code
  • and later, in different parts of the code a p2tr(...) call is made (without eccLib) and it works (the eccLib is cached)
  • someone removes/adds a condition for the first call (with eccLib). Then the second call (without eccLib) fails

Same goes for new Psbt({ eccLib: ecc }) vs new Psbt()

Or... are we planning to use the cache only for removing multiple verify calls for eccLib?

@junderw
Copy link

junderw commented Mar 22, 2022

We could do both.

Maybe even provide a public setter that will verify and set the cache so people don't have to use the weird options to begin with.

"Only insert this option once, any subsequent usage isn't needed" is a weird API.

@motorina0
Copy link
Collaborator

Maybe even provide a public setter that will verify and set the cache...

Yes. I would prefer something explicit as in the code snippet below for several reasons:

  • if the eccLib is invalid, then it will fail at the initEccLib(...) invocation place (which can make it easy to investigate/debug), instead of failing inside p2tr or Psbt
  • if p2tr is used without an eccLib then the error message can clearly state that: "No ECC Library provided. You must call initEccLib(...) with a valid TinySecp256k1Interface instance"
    • side note: rename TinySecp256k1Interface to Secp256k1Interface?
  • it makes the code-base a little easier to work with (no need to pass eccLib around)
  • it makes the public API a little simpler (no eccLib option for p2tr and Psbt)
  • calling initEccLib(...) is optional (only required if p2tr is used)
  • calling initEccLib(...) multiple times with the same TinySecp256k1Interface instance is a noop
// client code
import { payments, address, initEccLib } from 'bitcoinjs-lib'

initEccLib(clientEccLib)

@brandonblack
Copy link
Author

I'm in full support of an explicit initEccLib and then having p2tr throw a useful error if it's used prior to that being called.

@motorina0 would you like me to whip that up, or you?

@motorina0
Copy link
Collaborator

I would rather keep this PR smaller and focused on the review of the actual functionality.
I can do the initEccLib change. @junderw please 👍 if we have your blessing.

@OttoAllmendinger
Copy link

OttoAllmendinger commented Mar 23, 2022

My concern with initEccLib is that it will bloat the require statement. In a given file we typically cannot assume that initEccLib() was called somewhere else, so we will end up with extra boilerplate anywhere we import bitcoinjs-lib.

How about we wrap methods that require an eccLib in a class?

class PaymentBuilder<TEccLib extends EccLib> {
  constructor(private eccLib: TEccLib) { ... }
  
  p2tr(...) { /* uses this.eccLib */ }
}

This way we can constrain the allowed ecc libs via the type system.

(Disclaimer: like @brandonblack I work at BitGo)

@junderw
Copy link

junderw commented Mar 23, 2022

That boat has already sort of sailed with bip32/ecpair tbh.

The answer would be to have a single file that imports bitcoinjs, bip32/ecpair, and tiny-secp256k1, mashes them together, and re-exports the eccLib-included objects. Other files would then import from that one file.

@OttoAllmendinger
Copy link

That boat has already sort of sailed with bip32/ecpair tbh.

True, but unless I am missing something those deps do not have an init call.

If the recommendation is to wrap bitcoinjs-lib with crypto lib instances anyway, then I would favor the extra argument for consistency reason. Having a side-effecting call plus extra arguments is a strange mix.

@motorina0
Copy link
Collaborator

those deps do not have an init call.

They kind of have (its a factory pattern):

import BIP32Factory from 'bip32';
import ECPairFactory from 'ecpair';
import { payments, address, initEccLib } from 'bitcoinjs-lib'
import * as ecc from 'tiny-secp256k1';

const bip32 = BIP32Factory(ecc);
const ECPair = ECPairFactory(ecc);

// suggested change:
initEccLib(ecc)
...

Having a side-effecting call plus extra arguments is a strange mix.

  • it is either the initEccLib OR the extra argument ({ ..., eccLib?: TinySecp256k1Interface }). It does not make sense to have both
  • I don't know if having an init() method is a "side-effect" call, but I get your point. It its more like "enable extra functionality". The lib itself is stateless.
  • the "extra argument" has the disadvantage of having to "thread" eccLib (as @brandonblack correctly observed for Psbt)
  • the init() has the disadvantage that the client application must make sure to call it at least once.
  • other options... ideas are welcomed

@motorina0
Copy link
Collaborator

@brandonblack I would like to get these changes in the p2tr-v1 branch. Should I wait for the Psbt review, or is it OK for you to open a new PR?

@brandonblack
Copy link
Author

Fine with merging this as is. Still getting my head around PSBT, but hopefully others can continue reviewing in the meantime.

@junderw
Copy link

junderw commented Mar 24, 2022

Yeah merge this into the p2tr-v1 branch for now. We can continue discussion there.

@motorina0 motorina0 merged commit 6ea2f8b into bitcoincoretech:p2tr-v1 Mar 25, 2022
@motorina0
Copy link
Collaborator

PR merged! Thanks again @brandonblack
I have open this PR #5 against p2tr-v1 with the initEccLib chages.
Please have a look (and comment) before I merge it.

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.

5 participants