Skip to content

Commit

Permalink
Miniscript
Browse files Browse the repository at this point in the history
  • Loading branch information
scgbckbone authored and doc-hex committed Jun 20, 2023
1 parent 3cd47cd commit 594690d
Show file tree
Hide file tree
Showing 32 changed files with 5,707 additions and 1,367 deletions.
24 changes: 24 additions & 0 deletions docs/miniscript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Miniscript

**COLDCARD<sup>&reg;</sup>** Mk4 experimental `EDGE` versions
support Miniscript and MiniTapscript.

## Import/Export

* `Settings` -> `Miniscript` -> `Import from file`
* only [descriptors](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) allowed for import
* `Settings` -> `Miniscript` -> `<name>` -> `Descriptors`
* only [descriptors](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) are exported

## Address Explorer

Same as with basic multisig. After miniscript wallet is imported,
item with `<name>` is added to `Address Explorer` menu.


## Limitations
* no duplicate keys in miniscript (at least account in origin derivation has to be different)
* subderivation must be `<0;1>/*` (may be omitted during the import - is implied)
* only keys with key origin info `[xfp/p/a/t/h]xpub`
* maximum number of keys allowed in segwit v0 miniscript is 20
* check MiniTapscript limitations in `docs/taproot.md`
21 changes: 12 additions & 9 deletions docs/taproot.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# Taproot

**COLDCARD<sup>&reg;</sup>** Mk4 experimental `EDGE` versions will
**COLDCARD<sup>&reg;</sup>** Mk4 experimental `EDGE` versions
support Schnorr signatures ([BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)),
Taproot ([BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki))
and very limited Tapscript ([BIP-0342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki)) support.
Tapscript support will get more versatile with future iterations.
and Tapscript ([BIP-0342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki)) support.

## Output script (a.k.a address) generation

Expand All @@ -18,7 +17,11 @@ MUST be generated with above-mentoned methods to be considered change.
## Allowed descriptors

1. Single signature wallet without script path: `tr(key)`
2. Tapscript multisig with internal key and one sortedmulti multisig script: `tr(internal_key, sortedmulti_a(MofN))`
2. Tapscript multisig with internal key and up to 8 leaf scripts:
* `tr(internal_key, sortedmulti_a(2,@0,@1))`
* `tr(internal_key, pk(@0))`
* `tr(internal_key, {sortedmulti_a(2,@0,@1),pk(@2)})`
* `tr(internal_key, {or_d(pk(@0),and_v(v:pkh(@1),older(1000))),pk(@2)})`

## Provably unspendable internal key

Expand All @@ -27,24 +30,24 @@ for multisig.

1. use provably unspendable internal key H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs). This way is leaking the information that key path spending is not possible and therefore not recommended privacy-wise.

`tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, sortedmulti_a(MofN))`
`tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, sortedmulti_a(2,@0,@1))`

2. use COLDCARD specific placeholder `@` to let HWW pick a fresh integer r in the range 0...n-1 uniformly at random and use `H + rG` as internal key. COLDCARD will not store r and therefore user is not able to prove to other party how the key was generated and whether it is actually unspendable.

`tr(r=@, sortedmulti_a(MofN))`

3. pick a fresh integer r in the range 0...n-1 uniformly at random yourself and provide that in the descriptor. COLDCARD generates internal key with `H + rG`. It is possible to prove to other party that this internal key does not have a known discrete logarithm with respect to G by revealing r to a verifier who can then reconstruct how the internal key was created.

`tr(r=77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76, sortedmulti_a(MofN))`
`tr(r=77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76, sortedmulti_a(2,@0,@1))`


## Limitations

### Tapscript Limitations

In current version only `TREE` of depth 0 is allowed. Meaning that only one leaf script is allowed
and tagged hash of this single leaf script is also a merkle root. Only allowed script (for now) is `sortedmulti_a`.
Taproot multisig currently has artificial limit of max 32 signers (M=N=32).
In current version only `TREE` of max depth 4 is allowed (max 8 leaf script allowed).
Taproot single leaf multisig has artificial limit of max 32 signers (M=N=32).
Number of keys in taptree is limited to 32.

If Coldcard can sign by both key path and script path - key path has precedence.

Expand Down
4 changes: 3 additions & 1 deletion releases/EdgeChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
- for experimental use. DO NOT use for large Bitcoin amounts.
```

## 6.0.1X - 2023-0?-??
## 6.1.0X - 2023-06-19
- New Feature: Miniscript and MiniTapscript support (`docs/miniscript.md`)
- Enhancement: Tapscript up to 8 leafs
- Address explorer display refined slightly (cosmetic)

## 6.0.0X - 2023-05-12
Expand Down
2 changes: 1 addition & 1 deletion shared/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ async def start_login_sequence():
#
from ux import idle_logout
from glob import dis
import callgate
import callgate, version

if version.mk_num < 4:
# Block very obsolete versions.
Expand Down
86 changes: 31 additions & 55 deletions shared/address_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,14 @@
from menu import MenuSystem, MenuItem
from public_constants import AFC_BECH32, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
from uasyncio import sleep_ms
from ucollections import OrderedDict
from uhashlib import sha256
from ubinascii import hexlify as b2a_hex
from glob import settings
from auth import write_sig_file
from utils import addr_fmt_label
from utils import addr_fmt_label, truncate_address

def truncate_address(addr):
# Truncates address to width of screen, replacing middle chars
# - 16 chars screen width
# - but 2 lost at left (menu arrow, corner arrow)
# - want to show not truncated on right side
return addr[0:5] + '⋯' + addr[-6:]

class KeypathMenu(MenuSystem):
def __init__(self, path=None, nl=0):
Expand Down Expand Up @@ -201,7 +196,11 @@ async def render(self):
# if they have MS wallets, add those next
for ms in MultisigWallet.iter_wallets():
if not ms.addr_fmt: continue
items.append(MenuItem(ms.name, f=self.pick_multisig, arg=ms))
items.append(MenuItem(ms.name, f=self.pick_miniscript, arg=ms))

# if they have miniscript wallets, add those next
for msc in MiniScriptWallet.iter_wallets():
items.append(MenuItem(msc.name, f=self.pick_miniscript, arg=msc))
else:
items.append(MenuItem("Account: %d" % self.account_num, f=self.change_account))

Expand All @@ -223,10 +222,10 @@ async def pick_single(self, _1, _2, item):
settings.put('axi', axi) # update last clicked address
await self.show_n_addresses(path, addr_fmt, None)

async def pick_multisig(self, _1, _2, item):
ms_wallet = item.arg
async def pick_miniscript(self, _1, _2, item):
msc_wallet = item.arg
settings.put('axi', item.label) # update last clicked address
await self.show_n_addresses(None, None, ms_wallet)
await self.show_n_addresses(None, None, msc_wallet)

async def make_custom(self, *a):
# picking a custom derivation path: makes a tree of menus, with chance
Expand Down Expand Up @@ -256,7 +255,15 @@ async def show_n_addresses(self, path, addr_fmt, ms_wallet, start=0, n=10, allow
from glob import dis, NFC, VD
import version

def make_msg(change=0):
# speed up UI (do not recalculate addresses in while loop)
# cache can only have 2 values external, internal (0,1)
_cache = OrderedDict()

def make_msg(change=0, start=start, n=n):
nonlocal _cache
if (change, start, n) in _cache:
return _cache[(change, start, n)]

export_msg = "Press (1) to save Address summary file to SD Card."
if version.has_fatram and not ms_wallet:
export_msg += " Press (2) to view QR Codes."
Expand Down Expand Up @@ -288,26 +295,8 @@ def make_msg(change=0):
# but show enough they can verify addrs shown elsewhere.
# - makes a redeem script
# - converts into addr
# - assumes 0/0 is first address.
for (i, paths, addr, script, ik, ikp) in ms_wallet.yield_addresses(start, n, change_idx=change):
if i == 0 and ik:
msg += "Taproot internal key:\n\n"
if ikp:
msg += ikp + "\n\n"
else:
msg += '%s (provably unspendable)\n\n' % ik

if ms_wallet.N <= 4:
msg += "Taproot tree keys:\n\n"

if i == 0 and ms_wallet.N <= 4:
msg += '\n'.join(paths) + '\n =>\n'
else:
msg += '.../%d/%d =>\n' % (change, i)

addrs.append(addr)
msg += truncate_address(addr) + '\n\n'
dis.progress_bar_show(i/n)
# - assumes <0;1>/0 is first address.
msg, addrs = ms_wallet.make_addresses_msg(msg, start, n, change)

else:
# single-singer wallets
Expand All @@ -329,10 +318,15 @@ def make_msg(change=0):
if n > 1:
msg += "Press (9) to see next group, (7) to go back. X to quit."

if len(_cache) < 4:
_cache[(change, start, n)] = (msg, addrs)
else:
# LIFO
_cache = OrderedDict(list(_cache.items())[:-1])
return msg, addrs

msg, addrs = make_msg()
change = 0
msg, addrs = make_msg(change, start)
while 1:
ch = await ux_show_story(msg, escape='1234679')

Expand Down Expand Up @@ -380,31 +374,13 @@ def make_msg(change=0):
else:
continue # 3 in non-NFC mode

msg, addrs = make_msg(change)
msg, addrs = make_msg(change, start, n)

def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, change=0):
# Produce CSV file contents as a generator

if ms_wallet:
# For multisig, include redeem script and derivation for each signer
yield '"' + '","'.join(['Index', 'Payment Address',
'%s (%d of %d)' % (
"Leaf Script" if ms_wallet.internal_key else "Redeem Script",
ms_wallet.M, ms_wallet.N
)]
+ (['Derivation'] * ms_wallet.N)
+ ["Taproot Internal Key"] if ms_wallet.internal_key else []
) + '"\n'

for (idx, derivs, addr, script, ik, ikp) in ms_wallet.yield_addresses(start, n, change_idx=change):
ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode())
ln += '","'.join(derivs)
if ik:
# internal xonly key with its derivation (if any)
ln += '","%s' % (ikp + ik)
ln += '"\n'

yield ln
for line in ms_wallet.generate_address_csv(start, n, change):
yield line

return

Expand Down
30 changes: 16 additions & 14 deletions shared/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1391,17 +1391,15 @@ def usb_show_address(addr_format, subpath):
return active_request.address


class NewEnrollRequest(UserAuthorizedAction):
def __init__(self, ms, auto_export=False, bsms_index=None):
class NewMiniscriptEnrollRequest(UserAuthorizedAction):
def __init__(self, msc, auto_export=False, bsms_index=None):
super().__init__()
self.wallet = ms
self.wallet = msc
self.auto_export = auto_export
self.bsms_index = bsms_index

# self.result ... will be re-serialized xpub

async def interact(self):
from multisig import MultisigOutOfSpace
from wallet_base import WalletOutOfSpace

ms = self.wallet
try:
Expand All @@ -1413,23 +1411,23 @@ async def interact(self):
from bsms import BSMSSettings
BSMSSettings.signer_delete(self.bsms_index)
if self.auto_export:
# save cosigner details now too
await ms.export_wallet_file('created on',
"\n\nImport that file onto the other Coldcards involved with this multisig wallet.")
# save cosigner details now too
await ms.export_wallet_file('created on',
"\n\nImport that file onto the other Coldcards involved with this multisig wallet.")
await ms.export_electrum()

else:
# they don't want to!
self.refused = True
await ux_dramatic_pause("Refused.", 2)

except MultisigOutOfSpace:
except WalletOutOfSpace:
return await self.failure('No space left')
except BaseException as exc:
self.failed = "Exception"
sys.print_exception(exc)
finally:
UserAuthorizedAction.cleanup() # because no results to store
UserAuthorizedAction.cleanup() # because no results to store
if self.bsms_index is not None:
# bsms special case, get him back to multisig menu
from ux import the_ux, restore_menu
Expand All @@ -1445,9 +1443,10 @@ async def interact(self):
else:
self.pop_menu()

def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_index=None):
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_index=None, miniscript=False):
# Offer to import (enroll) a new multisig wallet. Allow reject by user.
from multisig import MultisigWallet
from miniscript import MiniScriptWallet

UserAuthorizedAction.cleanup()

Expand All @@ -1457,9 +1456,12 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_

# this call will raise on parsing errors, so let them rise up
# and be shown on screen/over usb
ms = MultisigWallet.from_file(config, name=name)
if miniscript:
msc = MiniScriptWallet.from_file(config, name=name)
else:
msc = MultisigWallet.from_file(config, name=name)

UserAuthorizedAction.active_request = NewEnrollRequest(ms, bsms_index=bsms_index)
UserAuthorizedAction.active_request = NewMiniscriptEnrollRequest(msc, bsms_index=bsms_index)

if ux_reset:
# for USB case, and import from PSBT
Expand Down
Loading

0 comments on commit 594690d

Please sign in to comment.