Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions releases/ChangeLog-mk4.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
option provided Settings -> Multisig Wallets -> Import via NFC. NFC has to be enabled
for this option to be visible.
- Bugfix: share single address over NFC from address explorer menu
- Enhancement: Allow import of new descriptor type which specify both internal/external in single string

## 5.0.6 - 2022-07-29

Expand Down
23 changes: 14 additions & 9 deletions shared/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def checksum_check(desc_w_checksum: str):
def parse_key_orig_info(key: str):
# key origin info is required for our MultisigWallet
close_index = key.find("]")
if key[0] != "[" and close_index == -1:
if key[0] != "[" or close_index == -1:
raise ValueError("Key origin info is required for %s" % (key))
key_orig_info = key[1:close_index] # remove brackets
key = key[close_index + 1:]
Expand All @@ -139,19 +139,21 @@ def parse_key_orig_info(key: str):

@staticmethod
def parse_key_derivation_info(key: str):
invalid_subderiv_msg = "Invalid subderivation path - only 0/* allowed"
invalid_subderiv_msg = "Invalid subderivation path - only 0/* or <0;1>/* allowed"
slash_split = key.split("/")
assert len(slash_split) > 1, invalid_subderiv_msg
if all(["h" not in elem and "'" not in elem for elem in slash_split[1:]]):
assert slash_split[1:] == ["0", "*"], invalid_subderiv_msg
assert slash_split[-1] == "*", invalid_subderiv_msg
assert slash_split[-2] in ["0", "<0;1>", "<1;0>"], invalid_subderiv_msg
assert len(slash_split[1:]) == 2, invalid_subderiv_msg
return slash_split[0]
else:
raise ValueError("Cannot use hardened sub derivation path")

def checksum(self):
return descriptor_checksum(self._serialize())

def serialize_keys(self, internal=False):
def serialize_keys(self, internal=False, int_ext=False):
result = []
for xfp, deriv, xpub in self.keys:
if deriv[0] == "m":
Expand All @@ -160,7 +162,10 @@ def serialize_keys(self, internal=False):
koi = xfp2str(xfp) + deriv
# normalize xpub to use h for hardened instead of '
key_str = "[%s]%s" % (koi.lower(), xpub)
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
if int_ext:
key_str = key_str + "/" + "<0;1>" + "/" + "*"
else:
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
result.append(key_str.replace("'", "h"))
return result

Expand Down Expand Up @@ -209,12 +214,12 @@ def parse(cls, desc_w_checksum: str) -> "MultisigDescriptor":
res_keys.append((xfp, origin_deriv, xpub))
return cls(M=M, N=N, keys=res_keys, addr_fmt=addr_fmt)

def _serialize(self, internal=False) -> str:
def _serialize(self, internal=False, int_ext=False) -> str:
"""Serialize without checksum"""
desc_base = FMT_TO_SCRIPT[self.addr_fmt]
desc_base = desc_base % ("sortedmulti(%s)")
assert len(self.keys) == self.N, "invalid descriptor object"
inner = str(self.M) + "," + ",".join(self.serialize_keys(internal=internal))
inner = str(self.M) + "," + ",".join(self.serialize_keys(internal=internal, int_ext=int_ext))
return desc_base % (inner)

def pretty_serialize(self) -> str:
Expand Down Expand Up @@ -249,9 +254,9 @@ def pretty_serialize(self) -> str:
checksum = self.serialize().split("#")[1]
return res % (inner) + "#" + checksum

def serialize(self, internal=False) -> str:
def serialize(self, internal=False, int_ext=False) -> str:
"""Serialize with checksum"""
return append_checksum(self._serialize(internal=internal))
return append_checksum(self._serialize(internal=internal, int_ext=int_ext))

def bitcoin_core_serialize(self):
res = []
Expand Down
11 changes: 4 additions & 7 deletions shared/nfc.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
# - has GPIO signal "??" which is multipurpose on its own pin
# - this chip chosen because it can disable RF interaction
#
import ngu, ckcc, utime
import ngu, ckcc, utime, ngu, ndef
from uasyncio import sleep_ms
from utils import B2A, problem_file_line
from ustruct import pack, unpack
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from ux import ux_wait_keyup, ux_show_story, ux_poll_key
import ndef
from ux import ux_show_story, ux_poll_key


# practical limit for things to share: 8k part, minus overhead
MAX_NFC_SIZE = const(8000)
Expand Down Expand Up @@ -490,9 +489,7 @@ async def selftest(cls):
async def share_file(self):
# Pick file from SD card and share over NFC...
from actions import file_picker
from ubinascii import unhexlify as a2b_hex
from files import CardSlot, CardMissingError
import ngu
from files import CardSlot, CardMissingError, needs_microsd

def is_suitable(fname):
f = fname.lower()
Expand Down
29 changes: 19 additions & 10 deletions testing/test_multisig.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def doit(config):
def import_ms_wallet(dev, make_multisig, offer_ms_import, need_keypress):

def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None, keys=None, do_import=True, derivs=None,
descriptor=False):
descriptor=False, int_ext_desc=False):
keys = keys or make_multisig(M, N, unique=unique, deriv=common or (derivs[0] if derivs else None))
name = name or f'test-{M}-{N}'

Expand All @@ -173,7 +173,10 @@ def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None, ke
assert len(derivs) == N
key_list = [(xfp, derivs[idx], dd.hwif(as_private=False)) for idx, (xfp, m, dd) in enumerate(keys)]
desc = MultisigDescriptor(M=M, N=N, keys=key_list, addr_fmt=addr_fmt)
desc_str = desc.serialize()
if int_ext_desc:
desc_str = desc.serialize(int_ext=True)
else:
desc_str = desc.serialize()
config = "%s\n" % desc_str
else:
# render as a file for import
Expand Down Expand Up @@ -878,7 +881,7 @@ def has_name(name, num_wallets=1):

menu = cap_menu()
assert f'{M}/{N}: {name}' in menu
assert len(menu) == 5 + num_wallets
assert len(menu) == 6 + num_wallets

title, story = offer_ms_import(make_named('xxx-orig'))
assert 'Create new multisig wallet' in story
Expand Down Expand Up @@ -1994,10 +1997,11 @@ def test_dup_ms_wallet_bug(goto_home, cap_story, pick_menu_item, cap_menu, need_

@pytest.mark.parametrize('M_N', [(2, 3), (2, 2), (3, 5), (6, 10), (15, 15)])
@pytest.mark.parametrize('addr_fmt', [ AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH ])
def test_import_desciptor(M_N, addr_fmt, offer_ms_import, import_ms_wallet, goto_home, pick_menu_item, clear_ms, need_keypress, cap_story, microsd_path):
@pytest.mark.parametrize('int_ext_desc', [True, False])
def test_import_desciptor(M_N, addr_fmt, int_ext_desc, offer_ms_import, import_ms_wallet, goto_home, pick_menu_item, clear_ms, need_keypress, cap_story, microsd_path):
clear_ms()
M, N = M_N
import_ms_wallet(M, N, addr_fmt=addr_fmt, accept=1, descriptor=True)
import_ms_wallet(M, N, addr_fmt=addr_fmt, accept=1, descriptor=True, int_ext_desc=int_ext_desc)

goto_home()
pick_menu_item('Settings')
Expand All @@ -2015,7 +2019,12 @@ def test_import_desciptor(M_N, addr_fmt, offer_ms_import, import_ms_wallet, goto
with open("debug/last-ms.txt", "r") as f:
desc_import = f.read().strip()
normalized = parse_desc_str(desc_export)
assert desc_import == normalized
# as new format is not widely supported we only allow to import it - no export yet
if int_ext_desc:
# checksum will differ - ignore it
assert desc_import.split("#")[0] == normalized.split("#")[0].replace("0/*", "<0;1>/*")
else:
assert desc_import == normalized
starts_with = FMT_TO_SCRIPT[addr_fmt].split("%")[0]
assert normalized.startswith(starts_with)
assert "sortedmulti(" in desc_export
Expand Down Expand Up @@ -2309,12 +2318,12 @@ def test_bitcoind_2of2_tutorial(desc_type, clear_ms, goto_home, need_keypress, p
@pytest.mark.parametrize("desc", [
("Missing descriptor checksum", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))"),
("Wrong checksum", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#gs2fqgl7"),
("Invalid subderivation path - only 0/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/1/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#sj7lxn0l"),
("Invalid subderivation path - only 0/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#fy9mm8dt"),
("Invalid subderivation path - only 0/* or <0;1>/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/1/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#sj7lxn0l"),
("Invalid subderivation path - only 0/* or <0;1>/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#fy9mm8dt"),
("Key origin info is required", "wsh(sortedmulti(2,tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#ypuy22nw"),
("Malformed key derivation info", "wsh(sortedmulti(2,[0f056943]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#nhjvt4wd"),
("Invalid subderivation path - only 0/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#gs2fqgl6"),
("Invalid subderivation path - only 0/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0))#s487stua"),
("Invalid subderivation path - only 0/* or <0;1>/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#gs2fqgl6"),
("Invalid subderivation path - only 0/* or <0;1>/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0))#s487stua"),
("Cannot use hardened sub derivation path", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0'/*))#3w6hpha3"),
("Unsupported descriptor", "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))#t2zpj2eu"),
("Unsupported descriptor", "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)#ml40v0wf"),
Expand Down