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

Add (sorted)multi_a descriptor for k-of-n multisig inside tr #24043

Merged
merged 7 commits into from
Mar 4, 2022

Conversation

sipa
Copy link
Member

@sipa sipa commented Jan 11, 2022

This adds a new multi_a(k,key_1,key_2,...,key_n) (and corresponding sortedmulti_a) descriptor for k-of-n policies inside tr(). Semantically it is very similar to the existing multi() descriptor, but with the following changes:

  • The corresponding script is <key1> OP_CHECKSIG <key2> OP_CHECKSIGADD <key3> OP_CHECKSIGADD ... <key_n> OP_CHECKSIGADD <k> OP_NUMEQUAL, rather than the traditional OP_CHECKMULTISIG-based script, making it usable inside the tr() descriptor.
  • The keys can optionally be specified in x-only notation.
  • Both the number of keys and the threshold can be as high as 999; this is the limit due to the consensus stacksize=1000 limit

I expect that this functionality will later be replaced with a miniscript-based implementation, but I don't think it's necessary to wait for that.

Limitations:

  • The wallet code will for not estimate witness size incorrectly for script path spends, which may result in a (dramatic) fee underpayment with large multi_a scripts.
  • The multi_a script construction is (slightly) suboptimal for n-of-n (where a <key1> OP_CHECKSIGVERIFY ... <key_n-1> OP_CHECKSIGVERIFY <key_n> OP_CHECKSIG would be better). Such a construction is not included here.

@sipa sipa changed the title Add multi_a descriptor for k-of-n multisig inside tr Add (sorted)multi_a descriptor for k-of-n multisig inside tr Jan 11, 2022
Copy link
Contributor

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

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

Left a few comments, did not review the tests.

src/script/descriptor.cpp Outdated Show resolved Hide resolved
src/script/standard.cpp Outdated Show resolved Hide resolved
src/script/standard.cpp Outdated Show resolved Hide resolved
src/script/standard.cpp Outdated Show resolved Hide resolved
@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 12, 2022

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #24149 (Signing support for Miniscript Descriptors by darosior)
  • #24148 (Miniscript support in Output Descriptors by darosior)
  • #24147 (Miniscript integration by darosior)
  • #23578 (Add external signer taproot support by Sjors)
  • #23502 (wallet: Avoid underpaying transaction fees when signing taproot spends by achow101)
  • #22838 (descriptors: Be able to specify change and receiving in a single descriptor string by achow101)
  • #22558 (psbt: Taproot fields for PSBT by achow101)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@luke-jr
Copy link
Member

luke-jr commented Jan 12, 2022

It seems like the distinction between the normal and _a variants is likely to cause confusion. Can we not support multi being smart enough to pick the right script? Is there a reason not to do that? How bad of a situation can exist if someone uses the wrong one?

@sipa
Copy link
Member Author

sipa commented Jan 12, 2022

@luke-jr I like to keep the invariant that every "function" in descriptors (and later, miniscript) refer to the same opcode construction.

The reason is that while for this example it's unambiguous (multi only in toplevel/sh/wsh, multi_a only in tr), that's not going to be the case for all (or in the case of miniscript, even most) functions. So there will inevitably be constructions with variants which cannot be derived from the context, and given that it seems more consistent to keep the contextual smartness out of this.

@luke-jr
Copy link
Member

luke-jr commented Jan 12, 2022

How about erroring if they're used in the wrong context?

@sipa
Copy link
Member Author

sipa commented Jan 12, 2022

@luke-jr They do (even before this PR).

@Sjors
Copy link
Member

Sjors commented Jan 12, 2022

The multi_a script construction is (slightly) suboptimal for n-of-n (where...

Isn't it also suboptimal for m-of-n where the permutations are sufficiently tractable to go in separate leaves? There may even be a privacy benefit to never revealing N. Neither is an objection to this descriptor though.

Copy link
Contributor

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

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

code review ACK 4828d53

Copy link
Member

@darosior darosior left a comment

Choose a reason for hiding this comment

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

Code review ACK 4828d53

Comment on lines +146 to +147
// Redundant, but very fast and selective test.
if (script.size() == 0 || script[0] != 32 || script.back() != OP_NUMEQUAL) return {};
Copy link
Member

Choose a reason for hiding this comment

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

if (script.size() > MAX_PUBKEYS_PER_MULTI_A * 34 + 3 + 1) return {};

Would also be a fast test bounding the potential number of iterations below.

Comment on lines +167 to +168
++it;
if (it != script.end()) return {};
Copy link
Member

Choose a reason for hiding this comment

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

nit: any reason to not combine these two lines?

std::vector<std::unique_ptr<PubkeyProvider>> keys;
keys.reserve(match->second.size());
for (const auto keyspan : match->second) {
if (keyspan.size() != 32) return {};
Copy link
Member

Choose a reason for hiding this comment

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

This cannot happen?

@@ -27,6 +27,7 @@
from .ripemd160 import ripemd160

MAX_SCRIPT_ELEMENT_SIZE = 520
MAX_PUBKEYS_PER_MULTI_A = 512
Copy link
Member

Choose a reason for hiding this comment

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

This should be 999.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, actually it's part of the next commit

@michaelfolkson
Copy link
Contributor

michaelfolkson commented Jan 31, 2022

Concept ACK, Approach ACK

I'll attempt to summarize some additional context that was discussed in Friday's Core wallet meeting that may be helpful for other reviewers too. (It was helpful for me. Apologies if I get any of this wrong and it needs a correction)

It seems like the distinction between the normal and _a variants is likely to cause confusion.

If you consider descriptors and Miniscript as one thing under a single name (which one day hopefully they will be, we already consider Miniscript as an extension to descriptors) then Miniscript already has suffixes such as and_b. The idea would be that the user would specify a Policy of multi (Policy doesn't have suffixes) and then the Policy to Miniscript compiler would determine whether it should be multi_a, multi_b etc depending on what the most efficient Miniscript is. This will most likely to be done outside of Core as the compiler isn't expected to be part of Core.

Can we not support multi being smart enough to pick the right script?

Each descriptor and each Miniscript is one-to-one with a specific script. I think multi at the Policy level will be able to pick the most efficient script using the Policy to Miniscript compiler. Still a lot of work to do though as Miniscript doesn't currently support Taproot.

Isn't it also suboptimal for m-of-n where the permutations are sufficiently tractable to go in separate leaves? There may even be a privacy benefit to never revealing N. Neither is an objection to this descriptor though.

As Sjors says there will be likely be many alternative Miniscripts that a Taproot multi Policy could compile down to. A 2-of-3 could be a single script path (a single Tapleaf) or it could be three 2-of-2s (one key path using MuSig2 and two script paths ) and that is before we have threshold key aggregation schemes (FROST etc) that could allow us to put the 2-of-3 directly in the key path.

@achow101
Copy link
Member

ACK 4828d53

@gruve-p
Copy link
Contributor

gruve-p commented Feb 21, 2022

ACK 4828d53

@maflcko maflcko added this to the 24.0 milestone Feb 21, 2022
@achow101 achow101 merged commit bada963 into bitcoin:master Mar 4, 2022
@michaelfolkson
Copy link
Contributor

@achow101: FYI there's some unresolved comments from @darosior above.

@darosior
Copy link
Member

darosior commented Mar 4, 2022 via email

@achow101
Copy link
Member

achow101 commented Mar 4, 2022

nits are non-blocking

sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Mar 5, 2022
…ig inside tr

4828d53 Add (sorted)multi_a descriptors to doc/descriptors.md (Pieter Wuille)
b5f33ac Simplify wallet_taproot.py functional test (Pieter Wuille)
eb0667e Add tests for (sorted)multi_a derivation/signing (Pieter Wuille)
c17c6aa Add signing support for (sorted)multi_a scripts (Pieter Wuille)
3eed6fc Add multi_a descriptor inference (Pieter Wuille)
79728c4 Add (sorted)multi_a descriptor and script derivation (Pieter Wuille)
25e95f9 Merge/generalize IsValidMultisigKeyCount/GetMultisigKeyCount (Pieter Wuille)

Pull request description:

  This adds a new `multi_a(k,key_1,key_2,...,key_n)` (and corresponding `sortedmulti_a`) descriptor for k-of-n policies inside `tr()`. Semantically it is very similar to the existing `multi()` descriptor, but with the following changes:
  * The corresponding script is `<key1> OP_CHECKSIG <key2> OP_CHECKSIGADD <key3> OP_CHECKSIGADD ... <key_n> OP_CHECKSIGADD <k> OP_NUMEQUAL`, rather than the traditional `OP_CHECKMULTISIG`-based script, making it usable inside the `tr()` descriptor.
  * The keys can optionally be specified in x-only notation.
  * Both the number of keys and the threshold can be as high as 999; this is the limit due to the consensus stacksize=1000 limit

  I expect that this functionality will later be replaced with a miniscript-based implementation, but I don't think it's necessary to wait for that.

  Limitations:
  * The wallet code will for not estimate witness size incorrectly for script path spends, which may result in a (dramatic) fee underpayment with large multi_a scripts.
  * The multi_a script construction is (slightly) suboptimal for n-of-n (where a `<key1> OP_CHECKSIGVERIFY ... <key_n-1> OP_CHECKSIGVERIFY <key_n> OP_CHECKSIG` would be better). Such a construction is not included here.

ACKs for top commit:
  achow101:
    ACK 4828d53
  gruve-p:
    ACK bitcoin@4828d53
  sanket1729:
    code review ACK 4828d53
  darosior:
    Code review ACK 4828d53

Tree-SHA512: 5dcd434b79585f0ff830f7d501d27df5e346f5749f47a3109ec309ebf2cbbad0e1da541eec654026d911ab67fd7cf7793fab0f765628d68d81b96ef2a4d234ce
@ajtowns
Copy link
Contributor

ajtowns commented Mar 7, 2022

The tr(H,multi_a(1,XPUB,XPRV)) test seems to be occasionally failing CI? https://cirrus-ci.com/task/4848707953754112 (through PSBT) https://cirrus-ci.com/task/6303111479296000 (address derivation) [EDIT: I've managed to reproduce this failure once on the master branch locally, but so far it hasn't repeated since I've added debug output]

@ajtowns
Copy link
Contributor

ajtowns commented Mar 7, 2022

I think the problem might be if different calls to do_test result in the same key being selected via keys = self.rand_keys(nkeys * 4) as in a previous invocation, then self.addr_gen will increment the derivation path from whatever was last used and choose /4 for addr_g, while addr_r will reuse the /0 path, and they'll get different results causing an assertion failure.

@achow101
Copy link
Member

achow101 commented Mar 7, 2022

I think the problem might be if different calls to do_test result in the same key being selected via keys = self.rand_keys(nkeys * 4) as in a previous invocation, then self.addr_gen will increment the derivation path from whatever was last used and choose /4 for addr_g, while addr_r will reuse the /0 path, and they'll get different results causing an assertion failure.

I'm seeing it fail with other indexes too. I think it's actually that multi_a and sortedmulti_a with the same keys will sometimes produce the same addresses for a given index. When the descriptor is imported, it finds that one of the previous addresses was used so it starts at an index other than 0, which is why we get the assertion failure.

@achow101
Copy link
Member

achow101 commented Mar 7, 2022

#24490 should fix the test failure

@ajtowns
Copy link
Contributor

ajtowns commented Mar 7, 2022

Makes sense!

@@ -33,6 +33,7 @@ Output descriptors currently support:
- Pay-to-taproot outputs (P2TR), through the `tr` function.
- Multisig scripts, through the `multi` function.
- Multisig scripts where the public keys are sorted lexicographically, through the `sortedmulti` function.
- Multisig scripts inside taproot script trees, through the `multi_a` (and `sortedmulti_a`) function.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this also be included in the 24.0 release notes?

Something like

tr() descriptors now support multisig scripts through the multi_a (and sortedmulti_a) function.

fanquake added a commit to bitcoin-core/gui that referenced this pull request Nov 25, 2022
…veaddresses

92a4ed0 doc: add tr() descriptor example to deriveaddresses (FractalEncrypt)

Pull request description:

  This simple PR adds a missing tr() descriptor example to the `help deriveaddresses` examples.

  - The functionality added in bitcoin/bitcoin#24043 is a significant departure from legacy multisig address creation, yet there is no corresponding tr() descriptor example in the help.
  - Having this example in combination with the examples in the descriptors documentation will be helpful to users.

  I needed this information to correctly create a tr multisig address but was unable. I had to leave the software and use a 3rd party site to ask two separate questions ([1](https://bitcoin.stackexchange.com/questions/115700/how-do-i-create-a-taproot-multisig-address-requiring-21-of-210-keys-to-spend), [2](https://bitcoin.stackexchange.com/questions/115742/signing-psbts-to-spend-from-taproot-multisig-address)) to create an address using the new functionality.

  Note: This specific example is not provided in the [descriptors.md ](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md) documentation, though there is a similar example with `sortedmulti_a. `

ACKs for top commit:
  instagibbs:
    ACK 92a4ed0
  kouloumos:
    ACK 92a4ed0
  w0xlt:
    ACK bitcoin/bitcoin@92a4ed0

Tree-SHA512: 8fb052bd469718157cb64439b885f8b0ecfb5a798535a02bae0a5dc748cd554a3e5ffdd9fe4acaef16156eadb59e1b2bcde7356e811397225f2783a84c8b112f
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Nov 25, 2022
92a4ed0 doc: add tr() descriptor example to deriveaddresses (FractalEncrypt)

Pull request description:

  This simple PR adds a missing tr() descriptor example to the `help deriveaddresses` examples.

  - The functionality added in bitcoin#24043 is a significant departure from legacy multisig address creation, yet there is no corresponding tr() descriptor example in the help.
  - Having this example in combination with the examples in the descriptors documentation will be helpful to users.

  I needed this information to correctly create a tr multisig address but was unable. I had to leave the software and use a 3rd party site to ask two separate questions ([1](https://bitcoin.stackexchange.com/questions/115700/how-do-i-create-a-taproot-multisig-address-requiring-21-of-210-keys-to-spend), [2](https://bitcoin.stackexchange.com/questions/115742/signing-psbts-to-spend-from-taproot-multisig-address)) to create an address using the new functionality.

  Note: This specific example is not provided in the [descriptors.md ](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md) documentation, though there is a similar example with `sortedmulti_a. `

ACKs for top commit:
  instagibbs:
    ACK 92a4ed0
  kouloumos:
    ACK 92a4ed0
  w0xlt:
    ACK bitcoin@92a4ed0

Tree-SHA512: 8fb052bd469718157cb64439b885f8b0ecfb5a798535a02bae0a5dc748cd554a3e5ffdd9fe4acaef16156eadb59e1b2bcde7356e811397225f2783a84c8b112f
delta1 added a commit to delta1/elements that referenced this pull request Oct 13, 2023
@bitcoin bitcoin locked and limited conversation to collaborators Oct 29, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.