Skip to content

Commit b39355f

Browse files
committed
Add Thresh Metr Musig writeup
1 parent be60698 commit b39355f

File tree

3 files changed

+231
-0
lines changed

3 files changed

+231
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ Scriptless scripts is an approach to designing cryptographic protocol on top of
1414
* Multi-hop locks are protocols that allow two parties to exchange coins and proof of payment without requiring a mutual funding multisig output (also known as "Lightning with Scriptless Scripts").
1515
* **[Non-Interactive Threshold Escrow (NITE)](md/NITE.md)**
1616
* NITE allows non-interactively setting up certain threshold policies on-chain, as well as off-chain if it is combined with [multi-hop locks](md/multi-hop-locks.md).
17+
* **[Thresh Metr MuSig](md/thresh-metr.md)**
18+
* This document discusses approaches to express THRESHold spending policies with MErkle TRees and MuSig.

md/thresh-metr.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Thresh Metr MuSig
2+
3+
This document discusses approaches to express THRESHold spending policies with MErkle TRees and MuSig.
4+
5+
## Introduction
6+
7+
There are multiple ways to set up a t-of-n threshold spending policy with Taproot.
8+
The most space efficient and private option is to use a threshold signature scheme like FROST to create a single public key and use that as the Taproot output key.
9+
No script spending and therefore no Taproot Merkle tree needed.
10+
However, threshold signatures are not always an option.
11+
For example, because storing the key shares is inconvenient, threshold signing isn't robust in general or simply because no suitable threshold implementation exists.
12+
13+
It is well known that a t-of-n threshold policy can also be implemented with a Taproot tree of t-of-t spending policies.
14+
15+
For example, a 2-of-3 policy can be expressed as a disjunction of three conjunctions:
16+
```
17+
2-of-{Alice, Bob, Charlie} = (Alice and Bob) or (Alice and Charlie) or (Bob and Charlie)
18+
```
19+
20+
Using MuSig key aggregation, one can build a Taproot tree for this policy as follows:
21+
```
22+
KeyAgg(A, B)
23+
| \
24+
| \
25+
| \
26+
KeyAgg(A, C) KeyAgg(B, C)
27+
```
28+
where the root is the Taproot output key and the leaf public keys are committed to the Merkle Tree along with the `OP_CHECKSIGVERIFY` opcode.
29+
30+
Compared to a `CHECKSIGADD`-based solution that always requires two signatures of three public keys, the advantage of this approach is better fungibility and efficiency.
31+
32+
We call a tree representing a t-of-n threshold policy *fully MuSig merkleized* if it purely consists of t-of-t spending conditions.
33+
The problem with such a tree is that the number of t-of-t conditions grows quickly (Example: n = 15, t = 11, "n choose t" = 15504).
34+
As a result the Merkle proofs become large for practical purposes (Example: log_2("n choose t") = 13).
35+
Moreover, in order to minimize the time to obtain a signature one is required to run the MuSig protocol for all t-of-t spending conditions in parallel, which is a large number in a fully MuSig merkleized tree.
36+
37+
## Assumption: Only up to k signers are non-cooperative most of the time
38+
39+
One idea to mitigate these issues comes from the observation that in many scenarios more than t parties are willing to sign.
40+
If we can bound the number of non-cooperative signers to be no more than some k < n - t, we can create a much more efficient spending tree, which we call *k MuSig merkleized*.
41+
42+
To set up such a tree, we start by creating a worst case spending condition, which is just a script with n public keys and a `CHECKSIGADD` opcode that requires t signatures.
43+
This is used as a fall back if more than k signers are non-cooperative.
44+
As in the case of fully MuSig merkleized trees, the remaining spending paths consist of *t-combinations* (combinations of size t) of the n public keys.
45+
46+
Now, knowing that only up to k signers are non-cooperative and we have a fall back in the form of a `CHECKSIGADD` path, we do not need all "n choose t"-many combinations in the tree.
47+
For example, a 3-of-5 threshold policy has t-combinations c0 = (0, 1, 2), c1 = (2, 3, 4), c2 = (1, 2, 3), etc.
48+
With k = 1 however, combination c2 is redundant.
49+
It would only be useful if signer 0 or signer 4 is absent, but in the former case we can use c1 and in the latter c2.
50+
51+
Let us define how a k MuSig merkleized tree for a t-of-n threshold looks like in general:
52+
- Let C be the t-combinations of n public keys.
53+
- Let D be the k-combinations of n public keys.
54+
- Let Cp be the smallest subset of C such that for all d in D there exists c in Cp such that the intersection of c and d is empty.
55+
56+
We want the MuSig key aggregate of every combination in Cp to appear in the tree.
57+
There are multiple ways to lay out the tree.
58+
For example, one of the combinations can be used as the taproot output key, the `CHECKSIGADD` fall back is a child of the and the rest of the combinations are arranged as a balanced tree that is the second child of the root key.
59+
60+
```
61+
KeyAgg(c in Cp)
62+
| \
63+
| \
64+
| \
65+
fall back script balanced tree of KeyAgg(c') for all c' in Cp\{c}
66+
```
67+
68+
The (inefficient and non-optimal) algorithm [thresh-metr.py](thresh-metr.py) gives the following output, demonstrating that k MuSig merkleized trees offer significant improvements over fully merkleized trees:
69+
70+
- 3-of-5 with up to 1 signers non-cooperative
71+
- Parallel signing sessions: 4
72+
- Everyone in key path cooperative: 1 sig, 1 pk: 96 WU
73+
- Up to 1 non-cooperative: 1 sig, 1 pk, 2 deep: 160 WU
74+
- More than 1 non-cooperative: 3 sig, 5 pk, 1 deep: 384 WU
75+
- In Comparison, fully merkleized multisig (6 parallel sessions): 1 sig, 1 pk, 4 deep: 224 WU
76+
- 11-of-15 with up to 2 signers non-cooperative
77+
- Parallel signing sessions: 32
78+
- Everyone in key path cooperative: 1 sig, 1 pk: 96 WU
79+
- Up to 2 non-cooperative: 1 sig, 1 pk, 6 deep: 288 WU
80+
- More than 2 non-cooperative: 11 sig, 15 pk, 1 deep: 1216 WU
81+
- In Comparison, fully merkleized multisig (1001 parallel sessions): 1 sig, 1 pk, 11 deep: 448 WU
82+
- 15-of-20 with up to 2 signers non-cooperative
83+
- Parallel signing sessions: 39
84+
- Everyone in key path cooperative: 1 sig, 1 pk: 96 WU
85+
- Up to 2 non-cooperative: 1 sig, 1 pk, 7 deep: 320 WU
86+
- More than 2 non-cooperative: 15 sig, 20 pk, 1 deep: 1632 WU
87+
- In Comparison, fully merkleized multisig (11628 parallel sessions): 1 sig, 1 pk, 14 deep: 544 WU
88+
- 15-of-20 with up to 3 signers non-cooperative
89+
- Parallel signing sessions: 248
90+
- Everyone in key path cooperative: 1 sig, 1 pk: 96 WU
91+
- Up to 3 non-cooperative: 1 sig, 1 pk, 9 deep: 384 WU
92+
- More than 3 non-cooperative: 15 sig, 20 pk, 1 deep: 1632 WU
93+
- In Comparison, fully merkleized multisig (11628 parallel sessions): 1 sig, 1 pk, 14 deep: 544 WU

md/thresh-metr.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import math
2+
from itertools import combinations as comb
3+
4+
def combinations(n, t):
5+
return [tuple(a) for a in comb(range(0, n), t)]
6+
7+
# Test whether the proposed spending paths Cp are actually sane
8+
def test_paths(Cp, n, t, k):
9+
if k > n - t:
10+
return False
11+
# no duplicates
12+
if len(Cp) != len(set(Cp)):
13+
return False
14+
for c in Cp:
15+
if len(c) != t:
16+
return False
17+
if max(c) >= n:
18+
return False
19+
D = combinations(n, k)
20+
for d in D:
21+
not_in_common = 0
22+
for c in Cp:
23+
c_set = set(c)
24+
d_set = set(d)
25+
if not (c_set & d_set):
26+
not_in_common = 1
27+
if not_in_common == 0:
28+
return False
29+
return True
30+
31+
# Test test_paths
32+
n = 5
33+
t = 3
34+
k = 1
35+
assert(test_paths(combinations(n, t), n, t, k))
36+
assert(not test_paths([(1,2,3)], n, t, k))
37+
k = 2
38+
assert(test_paths(combinations(n, t), n, t, k))
39+
k = 1
40+
# 2 have 2 common, 1 has only one common
41+
assert(test_paths([(1,2,3), (0,2,3), (0,1,4)], n, t, k))
42+
# doesn't work since 0 is always a required signer
43+
assert(not test_paths([(0,1,2), (0,2,3), (0,1,4)], n, t, k))
44+
45+
n = 6
46+
t = 4
47+
k = 1
48+
assert(test_paths(combinations(n, t), n, t, k))
49+
k = 2
50+
assert(test_paths(combinations(n, t), n, t, k))
51+
k = 1
52+
assert(test_paths([(1,2,3,4), (0,3,4,5), (0,1,2,5)], n, t, k))
53+
# has at most 2 common elements with every other
54+
55+
# Check if d is a subset in any element of Cpdiff
56+
def d_included(Cpdiff, d):
57+
for ci in Cpdiff:
58+
# if all elements are included
59+
if set(d).issubset(set(ci)):
60+
return True
61+
return False
62+
63+
# Minimum size of intersection between c and all elements of Cp
64+
def mininsect(Cp, c, n):
65+
m = n
66+
for cp in Cp:
67+
m_tmp = n - len(set(c).intersection(set(cp)))
68+
if m_tmp < m:
69+
m = m_tmp
70+
return m
71+
72+
# Generate t-of-n spending paths with up to k non-cooperative
73+
def generate_paths(n, t, k):
74+
a = set(range(0,n))
75+
C = combinations(n,t)
76+
D = combinations(n,k)
77+
Cp = []
78+
Cpdiff = []
79+
for d in D:
80+
if d_included(Cpdiff, d):
81+
continue
82+
# choose some c
83+
c_candidates = []
84+
for c in C:
85+
if not d_included([tuple(a.difference(set(c)))], d):
86+
continue
87+
if not c in Cp:
88+
c_candidates += [(c, mininsect(Cp, c, n))]
89+
c = max(c_candidates,key=lambda item:item[1])[0]
90+
Cp += [(c)]
91+
Cpdiff += [tuple(a.difference(set(c)))]
92+
return Cp
93+
94+
def cost(Cplen, n, t, k):
95+
sig = 64
96+
pk = 32
97+
branch = 32
98+
print("- %s-of-%s with up to %s signers non-cooperative" % (t, n, k))
99+
# + 1 for for the cooperative case
100+
spending_paths = Cplen + 1
101+
print(" - Parallel signing sessions:", spending_paths)
102+
print(" - Everyone in key path cooperative: 1 sig, 1 pk:", sig + pk, "WU")
103+
# only balanced tree part, i.e. exclude keypath and fallback
104+
tree_depth = math.ceil(1+math.log(spending_paths-2, 2))
105+
print(" - Up to %s non-cooperative: 1 sig, 1 pk, %s deep: %s WU" % (k, tree_depth, sig + pk + tree_depth*branch))
106+
print(" - More than %s non-cooperative: %s sig, %s pk, 1 deep: %s WU" % (k, t, n, t*sig + n*pk + branch))
107+
sessions = math.comb(n-1,t-1) # exclude combinations without the signer
108+
tree_depth = math.ceil(math.log(math.comb(n, t), 2))
109+
print(" - In Comparison, fully merkleized multisig (%s parallel sessions): 1 sig, 1 pk, %s deep: %s WU" % (sessions, tree_depth, sig + pk + tree_depth*branch))
110+
111+
# Examples
112+
n = 5
113+
t = 3
114+
k = 1
115+
Cp = generate_paths(n,t,k)
116+
cost(len(Cp), n, t, k)
117+
assert(test_paths(Cp, n, t, k))
118+
119+
n = 15
120+
t = 11
121+
k = 2
122+
Cp = generate_paths(n,t,k)
123+
cost(len(Cp), n, t, k)
124+
assert(test_paths(Cp, n, t, k))
125+
126+
n = 20
127+
t = 15
128+
k = 2
129+
Cp = generate_paths(n,t,k)
130+
cost(len(Cp), n, t, k)
131+
assert(test_paths(Cp, n, t, k))
132+
133+
k = 3
134+
Cp = generate_paths(n,t,k)
135+
cost(len(Cp), n, t, k)
136+
assert(test_paths(Cp, n, t, k))

0 commit comments

Comments
 (0)