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

Taproot multisig #4159

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

Taproot multisig #4159

wants to merge 13 commits into from

Conversation

andrewtoth
Copy link

@andrewtoth andrewtoth commented Sep 5, 2024

Adds basic taproot multisig support.

Uses a single OP_CHECKSIGADD-based script at the root level. Uses the secp256k1 generator as the basis for an xpub that derives an unspendable internal pubkey.

Resolves #1946

@andrewtoth
Copy link
Author

@prusnak @andrewkozlik @onvej-sl @matejcik Any feedback on this PR?

@matejcik
Copy link
Contributor

any feedback on this PR?

sure!
in general, we are interested in taproot multisig functionality.
we don't have it on the roadmap at the moment, so before anything happens on the PR, you'll need to wait for us to prioritize it.
personally I don't know the status of the various taproot multisig proposals, so i can't comment on whether your solution implements the one (or one of) that we want

in the future, please open an issue first where you outline your proposed solution. that way you can avoid doing all the work in case we say it's not something we are interested in 🤷‍♀️

@prusnak
Copy link
Member

prusnak commented Sep 10, 2024

The change is not so big and honestly it is worth merging in. Of course, we really need to have the new code properly tested.

Copy link
Member

@prusnak prusnak left a comment

Choose a reason for hiding this comment

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

Can we also add some tests that test the construction of a Taproot multisig transaction?

core/src/apps/bitcoin/common.py Outdated Show resolved Hide resolved
@andrewtoth
Copy link
Author

Thank you for the feedback @matejcik and @prusnak

in the future, please open an issue first

There is in fact an open issue, #1946, which I linked. The consensus in that issue is to implement the approach in this PR.

Also, this approach will be compatible with BIP387 descriptors tr(key, multi_a(k, key1, ... keyn)) and also the BIP388 wallet policy tr(@0/**, multi_a(k, @1/**, ..., @n/**)).

Can we also add some tests that test the construction of a Taproot multisig transaction?

Yes, I'd be happy to add tests and make any changes needed to have this feature merged and released. But first, I would like some assurance that this will in fact be merged. Would we be able to approve the workflows here so I can make sure I didn't miss anything else?

@andrewtoth
Copy link
Author

@matejcik @prusnak thank you - I have updated the PR to use a deterministic HDNode for the unspendable internal pubkey, and added tests for getting addresses and signing and serializing a tx.

@prusnak
Copy link
Member

prusnak commented Sep 13, 2024

I have updated the PR to use a deterministic HDNode for the unspendable internal pubkey

Why is this preferred to using a static dummy key from previously used code?

@andrewtoth
Copy link
Author

I have updated the PR to use a deterministic HDNode for the unspendable internal pubkey

Why is this preferred to using a static dummy key from previously used code?

This makes it compatible with BIP388 wallet policies. I cannot make a multisig wallet using a Trezor as one key and another hardware wallet that uses wallet policies as another key with just a static dummy key.

@prusnak
Copy link
Member

prusnak commented Sep 16, 2024

added tests for getting addresses and signing and serializing a tx.

Amazing!

@prusnak
Copy link
Member

prusnak commented Sep 16, 2024

I fixed smaller issues detected by the CI in commit e8cc7c4, but there is still bigger fish to fry:

$ cd core
$ make pyright                
python ../tools/pyright_tool.py

Ignored 88 custom-defined errors from 17 files.

ERROR: We have issues!

/Users/stick/work/trezor/trezor-firmware/core/src/apps/bitcoin/sign_tx/bitcoin.py:661: - error: Expression of type "tuple[bytes, bytes]" is incompatible with return type "bytes"
  "tuple[bytes, bytes]" is incompatible with "bytes" (reportReturnType)

/Users/stick/work/trezor/trezor-firmware/core/src/apps/bitcoin/sign_tx/bitcoin.py:677: - error: Argument of type "int" cannot be assigned to parameter "pubkey" of type "bytes" in function "multisig_pubkey_index"
  "int" is incompatible with "bytes" (reportArgumentType)

/Users/stick/work/trezor/trezor-firmware/core/src/apps/bitcoin/sign_tx/bitcoin.py:682: - error: Argument of type "int" cannot be assigned to parameter "signature" of type "bytes" in function "write_witness_multisig_taproot"
  "int" is incompatible with "bytes" (reportArgumentType)

/Users/stick/work/trezor/trezor-firmware/core/src/apps/bitcoin/sign_tx/bitcoin.py:688: - error: Argument of type "int" cannot be assigned to parameter "signature" of type "bytes" in function "write_witness_p2tr"
  "int" is incompatible with "bytes" (reportArgumentType)

/Users/stick/work/trezor/trezor-firmware/core/src/apps/bitcoin/sign_tx/bitcoin.py:713: - error: Argument of type "int | bytes" cannot be assigned to parameter "signature" of type "bytes" in function "set_serialized_signature"
  Type "int | bytes" is incompatible with type "bytes"
    "int" is incompatible with "bytes" (reportArgumentType)

/Users/stick/work/trezor/trezor-firmware/core/src/apps/bitcoin/sign_tx/decred.py:168: - error: Expression of type "DecredSigHasher" is incompatible with return type "SigHasher"
  "DecredSigHasher" is incompatible with protocol "SigHasher"
    "hash341" is an incompatible type
      Type "(i: int, tx: SignTx | PrevTx, sighash_type: SigHashType) -> bytes" is incompatible with type "(i: int, tx: SignTx | PrevTx, sighash_type: SigHashType, leaf_hash: bytes | None) -> bytes"
        Function accepts too many positional parameters; expected 3 but received 4 (reportReturnType)

/Users/stick/work/trezor/trezor-firmware/core/src/trezor/utils.py:282: - error: Expression of type "memoryview[_I@memoryview] | memoryview[int]" is incompatible with return type "memoryview[int]"
  Type "memoryview[_I@memoryview] | memoryview[int]" is incompatible with type "memoryview[int]"
    "memoryview[_I@memoryview]" is incompatible with "memoryview[int]"
      Type parameter "_I@memoryview" is invariant, but "_I@memoryview" is not the same as "int" (reportReturnType)

/Users/stick/work/trezor/trezor-firmware/core/src/trezor/utils.py:292: - error: Expression of type "_I@memoryview | int" is incompatible with return type "int"
  Type "object* | int" is incompatible with type "int"
    "object*" is incompatible with "int" (reportReturnType)

/Users/stick/work/trezor/trezor-firmware/core/src/trezor/utils.py:300: - error: Expression of type "_I@memoryview | int" is incompatible with return type "int"
  Type "object* | int" is incompatible with type "int"
    "object*" is incompatible with "int" (reportReturnType)

Found 9 issues above
make: *** [pyright] Error 1

@andrewtoth
Copy link
Author

For me

$ cd core
$ make pyright  

returns

python ../tools/pyright_tool.py
Error: [Errno 2] No such file or directory: 'pyright'
make: *** [Makefile:211: pyright] Error 1

I couldn't find much docs on setting up pyright. Any hints?

@prusnak
Copy link
Member

prusnak commented Sep 16, 2024

I couldn't find much docs on setting up pyright. Any hints?

Hm. Not sure - maybe npm install -g pyright@1.1.361?

Or you can use pyright from brew, if you are no macOS.

@andrewtoth
Copy link
Author

Thanks! After my last commit I get:

python ../tools/pyright_tool.py

Ignored 88 custom-defined errors from 17 files.

SUCCESS: Everything is fine!

@andrewtoth
Copy link
Author

I get an error that I need to add a changelog as well. Should I do that or will you?

@prusnak
Copy link
Member

prusnak commented Sep 16, 2024

I get an error that I need to add a changelog as well. Should I do that or will you?

I added it in my commit already

@andrewtoth
Copy link
Author

Ah I see legacy tests are failing. I only tested this on Model T with core. Can this feature be disabled on legacy devices, or is legacy support required for this?

@prusnak
Copy link
Member

prusnak commented Sep 16, 2024

Can this feature be disabled on legacy devices, or is legacy support required for this?

We can disable the tests with

@pytest.mark.models(skip=["legacy"])

@andrewtoth
Copy link
Author

For the device tests, it appears I need to add hashes for each device and each language. Could that be done in an automated step by CI?

@andrewtoth
Copy link
Author

@prusnak I'm unsure how to proceed here. I don't have any context about what the Post comment with UI diff URLs jobs are and why they are failing. There's also a Changelog check job failing that seems to be looking for a legacy changelog but there hasn't been anything touched in legacy.

Is there anything else I need to do here?

@prusnak
Copy link
Member

prusnak commented Sep 20, 2024

@prusnak I'm unsure how to proceed here.

Let's leave that to @matejcik or someone else from the firmware team. I am sure the fix is trivial, but for me or you it would take hours to figure out.

Also legacy needs a changelog entry because we touched the code in the legacy/ folder (added extra parameter to some functions). Again, let's have the firmware team to either add a dummy changelog entry or find a way how to suppress the change.

Again, thank you for this massive contribution and we'll take it from here.

crypto/tests/test_check.c Outdated Show resolved Hide resolved
depth=0,
fingerprint=2084970077,
child_num=0,
chain_code=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
Copy link
Author

Choose a reason for hiding this comment

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

I think we also want to allow passing in the chain code to create a randomized xpub that still uses the NUMS point public key. That way the derived public key is provably unspendable, but cannot be fingerprinted on chain.
We will need to define a new parameter for this in the MultisigRedeemScriptType.

Comment on lines +101 to +103
for i in multisig.address_n:
node.derive(i, True)
return node.public_key()
Copy link
Contributor

Choose a reason for hiding this comment

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

This is something I don't understand: The dummy pubkey is derived using multisig.address_n. Suppose we have two wallets. The first one uses a node HD1 and the other a node HD2. Suppose we have a multisig address created from public keys HD1/0/0 and HD1/0/1. Since the wallets use different paths to derive the dummy pubkey, they will generate different addresses. Consequently, if the first wallet creates a transaction with the multisig address as an output, the second wallet will create a valid witness with a probability of 1/2, because the parity of the tweaked dummy key is used in the witness. Is this correct?

Copy link
Author

Choose a reason for hiding this comment

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

But address_n must be the same for the entire MultisigRedeemScriptType, so how can the address be created by HD1/0/0 and HD1/0/1? That would imply an address_n of [0,0] for the first key and [0,1] for the second. Am I misunderstanding?

Copy link
Contributor

Choose a reason for hiding this comment

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

I was under the impression that you could specify address_n for each pubkey separately in multisig.pubkeys[*].address_n.

return [multisig_get_pubkey(hd.node, hd.address_n) for hd in multisig.pubkeys]

Copy link
Author

Choose a reason for hiding this comment

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

Oh I see you can have pubkeys or nodes, and pubkeys have their own address_n individually. I haven't tested this with that. I suppose we can disable using pubkeys list for taproot multisig?
Is pubkeys list legacy or is it still widely used?

Copy link
Contributor

Choose a reason for hiding this comment

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

By the way, I'm going to do a quick research to see what internal keys other wallets that support Taproot multisig use. If we want other wallets to be interoperable with trezor, we should follow the same approach.

Copy link
Author

Choose a reason for hiding this comment

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

@darosior already implemented a deterministic derivation for the chaincode in Liana; it would be great if new implementations of the deterministic derivations are kept compatible.

I don't think it's great that it depends on the order of keys provided. If we want to sort the pubkeys beforehand and order the xpubs in the order of the derived pubkeys, that would make a different chaincode for each order.

I think we should sort the xpubs before hashing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, it's great to see that someone else came to essentially the same solution. Seems like we are on the right path. To make sure that we are all on the same page, let me summarize the current plan:

  • The dummy XPUB that is used to derive the internal public key will use the NUMS public key specified in BIP 341 and a chain code deterministically derived from the multisig XPUBs.
  • MultisigRedeemScriptType doesn't change. Trezor derives the dummy XPUB internally.
  • All the public keys in the multisig, including the internal public key will use the same derivation path.
  • To keep things simple, I would disable MultisigRedeemScriptType.pubkeys for taproot multisig, i.e. enforce the use of MultisigRedeemScriptType.nodes. There is nothing stopping us from loosening this restriction in the future if necessary.

The question remains how to derive the chain code. This is tricky. Trezor doesn't deal with policies or descriptors directly at the moment, at least not for multisig. Right now, distinguishing between sortedmulti_a() and multi_a() would be up to the host application, and it's up to that application to fill in the MultisigRedeemScriptType in the correct order. So if sortedmulti_a() is specified, it needs to derive the public keys and sort the XPUBs accordingly in MultisigRedeemScriptType.

It seems to me that we can't just take the order of the XPUBs given in MultisigRedeemScriptType, because with sortedmulti_a() it would make the dummy XPUB depend on the path, since the host changes the XPUB order for each path. That would presumably be incompatible with Ledger, not to mention confusing. So unless we overhaul MultisigRedeemScriptType to be able to handle descriptors, we need the dummy XPUB derivation to be independent of the XPUB order. Meaning they need to be sorted somehow, as you proposed, or the derivation operation needs to be order-agnostic, e.g. dummy chain code = XOR of the multisig chain codes. The XOR is not great, because it allows a malicious participant to set the chain code to a desired value, but come to think of it, perhaps that not an issue.

All that being said, I'd very much hope for the solution to be compatible with existing implementations, and ideally with whatever will be the future specification, because sooner or later this will need to be standardized to ensure smooth interoperability amongst wallets. Hopefully your comment in delvingbitcoin.org will spark some discussion about that.

Choose a reason for hiding this comment

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

It seems to me that we can't just take the order of the XPUBs given in MultisigRedeemScriptType, because with sortedmulti_a() it would make the dummy XPUB depend on the path, since the host changes the XPUB order for each path. That would presumably be incompatible with Ledger, not to mention confusing.

Ledger's implementation doesn't care how the chaincode of the unspendable key is computed, as it's explicitly passed on by the client together with all the other xpubs. It does recognize it as unspendable as long as the pubkey is the standard NUMS point, and that's important for security - or the client could lie and put an actual spendable pubkey.

Using the deterministic derivation only matters for UX (the device could in principle skip showing it altogether in that case); this is currently left for future improvements to the wallet policies / miniscript UX.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am somewhat disappointed that our comments in delvingbitcoin.org did not spark any feedback. The way I see it, we are practically facing these options:

  1. The host will supply the chaincode for the unspendable XPUB and the user will confirm it on Trezor. This has poor UX. As if multisig signing wasn't hard enough to use for laymen, we add yet another layer of complexity for them to understand.
  2. Design our own scheme that derives the chaincode for the unspendable XPUB from the multisig XPUBs independently of their order. Poor interoperability, e.g. with Liana. All we can do is hope that our design is appealing enough that others adopt it.
  3. Adopt the Liana scheme, which in my opinion is not the best choice.

If a standard derivation scheme arises in the future, that is not identical with our choice, we will need to maintain the old scheme somewhere in our code, which I would like to avoid if we can. We currently don't have the capacity to push the standardization process forward. @andrewtoth would you be willing to do that? Even starting a discussion on the Bitcoin mailing list could be a step forward.

Copy link
Author

Choose a reason for hiding this comment

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

@andrewkozlik thanks for that summary. I agree, we should ideally have a BIP for this before merging here. #1 would be safest option in that sense since we can guarantee we will be forward compatible with any standard that emerges. But, the UX drawback is indeed unfortunate.

I do think there is already a risk with Trezor not showing all xpubs being used in a multisig address, for the same reason the dummy xpub needs to be shown. A single trezor user must take for granted all the other xpubs used in creating the multisig address shown. Ledger solves this by storing this information as a wallet policy that only has to be confirmed once.

I will be meeting with some of the other developers who have commented in that delving thread next week. I will be sure to discuss this with them.

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.

Implement multisig for Taproot
6 participants