Skip to content

Commit a5c9926

Browse files
authored
Expand detection of possible multisig descriptors & Add data input parsing CI tests (#301)
1 parent 339eea9 commit a5c9926

File tree

9 files changed

+398
-25
lines changed

9 files changed

+398
-25
lines changed

.github/workflows/test.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
native-tests:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v4
13+
- name: Set up Python
14+
uses: actions/setup-python@v5
15+
with:
16+
python-version: '3.11'
17+
- name: Install dependencies
18+
run: |
19+
python3 -m pip install --upgrade pip
20+
pip install -r requirements.txt -r test/integration/requirements.txt
21+
- name: Run native unit tests
22+
working-directory: test
23+
run: |
24+
python3 run_native_tests.py
25+
python3 -m compileall ../src ../test
26+
27+
tests:
28+
runs-on: ubuntu-latest
29+
steps:
30+
- name: Checkout
31+
uses: actions/checkout@v4
32+
- name: Install dependencies
33+
run: |
34+
sudo apt-get update
35+
sudo apt-get install -y \
36+
build-essential \
37+
libffi-dev \
38+
libgmp-dev \
39+
libreadline-dev \
40+
libsdl2-dev \
41+
pkg-config \
42+
python3
43+
- name: Run tests
44+
run: make test

Makefile

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ BOARD ?= STM32F469DISC
33
FLAVOR ?= SPECTER
44
USER_C_MODULES ?= ../../../usermods
55
MPY_DIR ?= f469-disco/micropython
6+
MPY_CFLAGS ?= -Wno-dangling-pointer -Wno-enum-int-mismatch
67
FROZEN_MANIFEST_DISCO ?= ../../../../manifests/disco.py
78
FROZEN_MANIFEST_DEBUG ?= ../../../../manifests/debug.py
89
FROZEN_MANIFEST_UNIX ?= ../../../../manifests/unix.py
@@ -20,55 +21,59 @@ $(MPY_DIR)/mpy-cross/Makefile:
2021
mpy-cross: $(TARGET_DIR) $(MPY_DIR)/mpy-cross/Makefile
2122
@echo Building cross-compiler
2223
make -C $(MPY_DIR)/mpy-cross \
23-
DEBUG=$(DEBUG) && \
24+
DEBUG=$(DEBUG) \
25+
CFLAGS_EXTRA="$(MPY_CFLAGS)" && \
2426
cp $(MPY_DIR)/mpy-cross/mpy-cross $(TARGET_DIR)
2527

2628
# disco board with bitcoin library
2729
disco: $(TARGET_DIR) mpy-cross $(MPY_DIR)/ports/stm32
2830
@echo Building firmware
2931
make -C $(MPY_DIR)/ports/stm32 \
30-
BOARD=$(BOARD) \
31-
FLAVOR=$(FLAVOR) \
32-
USE_DBOOT=$(USE_DBOOT) \
33-
USER_C_MODULES=$(USER_C_MODULES) \
34-
FROZEN_MANIFEST=$(FROZEN_MANIFEST_DISCO) \
35-
DEBUG=$(DEBUG) && \
32+
BOARD=$(BOARD) \
33+
FLAVOR=$(FLAVOR) \
34+
USE_DBOOT=$(USE_DBOOT) \
35+
USER_C_MODULES=$(USER_C_MODULES) \
36+
FROZEN_MANIFEST=$(FROZEN_MANIFEST_DISCO) \
37+
DEBUG=$(DEBUG) \
38+
CFLAGS_EXTRA="$(MPY_CFLAGS)" && \
3639
arm-none-eabi-objcopy -O binary \
37-
$(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.elf \
38-
$(TARGET_DIR)/specter-diy.bin && \
39-
cp $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.hex \
40-
$(TARGET_DIR)/specter-diy.hex
40+
$(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.elf \
41+
$(TARGET_DIR)/specter-diy.bin && \
42+
cp $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.hex \
43+
$(TARGET_DIR)/specter-diy.hex
4144

4245
# disco board with bitcoin library
4346
debug: $(TARGET_DIR) mpy-cross $(MPY_DIR)/ports/stm32
4447
@echo Building firmware
4548
make -C $(MPY_DIR)/ports/stm32 \
46-
BOARD=$(BOARD) \
47-
FLAVOR=$(FLAVOR) \
48-
USE_DBOOT=$(USE_DBOOT) \
49-
USER_C_MODULES=$(USER_C_MODULES) \
50-
FROZEN_MANIFEST=$(FROZEN_MANIFEST_DEBUG) \
51-
DEBUG=$(DEBUG) && \
49+
BOARD=$(BOARD) \
50+
FLAVOR=$(FLAVOR) \
51+
USE_DBOOT=$(USE_DBOOT) \
52+
USER_C_MODULES=$(USER_C_MODULES) \
53+
FROZEN_MANIFEST=$(FROZEN_MANIFEST_DEBUG) \
54+
DEBUG=$(DEBUG) \
55+
CFLAGS_EXTRA="$(MPY_CFLAGS)" && \
5256
arm-none-eabi-objcopy -O binary \
53-
$(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.elf \
54-
$(TARGET_DIR)/debug.bin && \
57+
$(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.elf \
58+
$(TARGET_DIR)/debug.bin && \
5559
cp $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.hex \
56-
$(TARGET_DIR)/debug.hex
60+
$(TARGET_DIR)/debug.hex
5761

5862

5963
# unixport (simulator)
6064
unix: $(TARGET_DIR) mpy-cross $(MPY_DIR)/ports/unix
6165
@echo Building binary with frozen files
6266
make -C $(MPY_DIR)/ports/unix \
63-
USER_C_MODULES=$(USER_C_MODULES) \
64-
FROZEN_MANIFEST=$(FROZEN_MANIFEST_UNIX) && \
67+
USER_C_MODULES=$(USER_C_MODULES) \
68+
FROZEN_MANIFEST=$(FROZEN_MANIFEST_UNIX) \
69+
CFLAGS_EXTRA="$(MPY_CFLAGS)" && \
6570
cp $(MPY_DIR)/ports/unix/micropython $(TARGET_DIR)/micropython_unix
6671

6772
simulate: unix
6873
$(TARGET_DIR)/micropython_unix simulate.py
6974

7075
test: unix
71-
$(TARGET_DIR)/micropython_unix tests/run_tests.py
76+
cd test && ../$(TARGET_DIR)/micropython_unix run_tests.py
7277

7378
all: mpy-cross disco unix
7479

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ Specter Shield-Lite documentation is available in the [`shield-lite/`](./shield-
5353

5454
Supported networks: Mainnet, Testnet, Regtest, Signet.
5555

56+
## Running tests
57+
58+
The unit test suite runs on the Unix simulator build. Install the required
59+
system packages and then run the `make` target:
60+
61+
```
62+
sudo apt-get update
63+
sudo apt-get install libsdl2-dev libffi-dev pkg-config libreadline-dev libgmp-dev build-essential python3
64+
make test
65+
```
66+
67+
The build system will fetch the necessary submodules and compile the simulator
68+
before executing the tests.
69+
5670
## USB communication on Linux
5771

5872
You may need to set up udev rules and add yourself to `dialout` group. Read more in [`udev`](./udev/README.md) folder.

src/apps/wallets/manager.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ def parse_stream(self, stream):
175175
except:
176176
pass
177177
# probably wallet descriptor
178-
if b"&" in data and b"?" not in data:
178+
# Check for an & Symbol, typically used when descriptor supplied with a name
179+
# Also check the most common descriptor types for multisig wallets
180+
common_descriptor_markers = [b"&", b"tr(", b"wsh(", b"sh("]
181+
if any(marker in data for marker in common_descriptor_markers) and b"?" not in data:
179182
# rewind
180183
stream.seek(0)
181184
return ADD_WALLET, stream

test/native_support.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import os
2+
import sys
3+
import types
4+
5+
6+
def _ensure_module(name):
7+
mod = sys.modules.get(name)
8+
if mod is None:
9+
mod = types.ModuleType(name)
10+
sys.modules[name] = mod
11+
return mod
12+
13+
14+
def _ensure_submodule(package, name, attrs):
15+
full_name = f"{package}.{name}"
16+
module = _ensure_module(full_name)
17+
for attr, value in attrs.items():
18+
if not hasattr(module, attr):
19+
setattr(module, attr, value)
20+
parent = _ensure_module(package)
21+
if not hasattr(parent, "__path__"):
22+
parent.__path__ = []
23+
setattr(parent, name, module)
24+
return module
25+
26+
27+
def setup_native_stubs():
28+
if sys.implementation.name == 'micropython':
29+
return
30+
31+
if not hasattr(os, "ilistdir"):
32+
def _ilistdir(path):
33+
for entry in os.scandir(path):
34+
mode = 0x4000 if entry.is_dir() else 0x8000
35+
yield (entry.name, mode, 0, 0)
36+
os.ilistdir = _ilistdir
37+
38+
pyb = _ensure_module("pyb")
39+
if not hasattr(pyb, "SDCard"):
40+
class _DummySDCard:
41+
def __init__(self, *args, **kwargs):
42+
pass
43+
44+
def present(self):
45+
return True
46+
47+
def power(self, value):
48+
pass
49+
50+
class _DummyLED:
51+
def __init__(self, *args, **kwargs):
52+
pass
53+
54+
def on(self):
55+
pass
56+
57+
def off(self):
58+
pass
59+
60+
pyb.SDCard = _DummySDCard
61+
pyb.LED = _DummyLED
62+
pyb.usb_mode = lambda *args, **kwargs: None
63+
pyb.UART = lambda *args, **kwargs: None
64+
pyb.USB_VCP = lambda *args, **kwargs: None
65+
66+
lvgl = _ensure_module("lvgl")
67+
if not hasattr(lvgl, "SYMBOL"):
68+
class _Symbol:
69+
EDIT = "[edit]"
70+
TRASH = "[trash]"
71+
72+
def __getattr__(self, name):
73+
return f"[{name.lower()}]"
74+
75+
lvgl.SYMBOL = _Symbol()
76+
77+
display = _ensure_module("display")
78+
if not hasattr(display, "Screen"):
79+
display.Screen = type("Screen", (), {})
80+
81+
gui = _ensure_module("gui")
82+
if not hasattr(gui, "__path__"):
83+
gui.__path__ = []
84+
85+
screens = _ensure_module("gui.screens")
86+
if not hasattr(screens, "__path__"):
87+
screens.__path__ = []
88+
for _name in [
89+
"Menu",
90+
"InputScreen",
91+
"Prompt",
92+
"TransactionScreen",
93+
"WalletScreen",
94+
"ConfirmWalletScreen",
95+
"QRAlert",
96+
"Alert",
97+
"PinScreen",
98+
"DerivationScreen",
99+
"NumericScreen",
100+
"MnemonicScreen",
101+
"NewMnemonicScreen",
102+
"RecoverMnemonicScreen",
103+
"Progress",
104+
"DevSettings",
105+
]:
106+
if not hasattr(screens, _name):
107+
setattr(screens, _name, type(_name, (), {}))
108+
109+
_ensure_submodule("gui.screens", "mnemonic", {
110+
"ExportMnemonicScreen": type("ExportMnemonicScreen", (), {}),
111+
})
112+
_ensure_submodule("gui.screens", "settings", {
113+
"HostSettings": type("HostSettings", (), {}),
114+
})
115+
_ensure_submodule("gui.screens", "screen", {
116+
"Screen": type("Screen", (), {}),
117+
})
118+
_ensure_submodule("gui.screens", "qralert", {
119+
"QRAlert": type("QRAlert", (), {}),
120+
})
121+
122+
common = _ensure_module("gui.common")
123+
if not hasattr(common, "HOR_RES"):
124+
common.HOR_RES = 480
125+
if not hasattr(common, "styles"):
126+
common.styles = types.SimpleNamespace()
127+
for _name in [
128+
"add_label",
129+
"add_button",
130+
"add_button_pair",
131+
"align_button_pair",
132+
"format_addr",
133+
]:
134+
if not hasattr(common, _name):
135+
setattr(common, _name, lambda *args, **kwargs: None)
136+
137+
decorators = _ensure_module("gui.decorators")
138+
if not hasattr(decorators, "on_release"):
139+
decorators.on_release = lambda func: func
140+
141+
ucryptolib = _ensure_module("ucryptolib")
142+
if not hasattr(ucryptolib, "aes"):
143+
class _DummyAES:
144+
def __init__(self, key, mode, iv):
145+
self.key = key
146+
self.mode = mode
147+
self.iv = iv
148+
149+
def encrypt(self, data):
150+
return data
151+
152+
def decrypt(self, data):
153+
return data
154+
155+
ucryptolib.aes = lambda key, mode, iv: _DummyAES(key, mode, iv)
156+
157+
bcur = _ensure_module("bcur")
158+
if not hasattr(bcur, "bcur_decode_stream"):
159+
bcur.bcur_decode_stream = lambda stream: stream
160+
161+
secp256k1 = _ensure_module("secp256k1")
162+
if not hasattr(secp256k1, "EC_UNCOMPRESSED"):
163+
secp256k1.EC_UNCOMPRESSED = 0
164+
secp256k1.ec_pubkey_parse = lambda data: data
165+
secp256k1.ec_pubkey_create = lambda secret: secret
166+
secp256k1.ec_pubkey_serialize = lambda pub, flag=0: b"\x04" + bytes(64)
167+
secp256k1.ec_pubkey_tweak_mul = lambda pub, secret: None
168+
secp256k1.ecdsa_signature_parse_der = lambda raw: raw
169+
secp256k1.ecdsa_signature_normalize = lambda sig: sig
170+
secp256k1.ecdsa_verify = lambda sig, msg, pub: True
171+
secp256k1.ecdsa_sign_recoverable = lambda msghash, secret: bytes(65)
172+
173+
from app import BaseApp
174+
175+
if not hasattr(BaseApp, "_native_original_get_prefix"):
176+
BaseApp._native_original_get_prefix = BaseApp.get_prefix
177+
178+
def _native_get_prefix(self, stream):
179+
pos = stream.tell()
180+
prefix = BaseApp._native_original_get_prefix(self, stream)
181+
if prefix is not None:
182+
prefixes = getattr(self, 'prefixes', None)
183+
if prefixes and prefix not in prefixes:
184+
stream.seek(pos)
185+
return None
186+
return prefix
187+
188+
BaseApp.get_prefix = _native_get_prefix
189+
190+
try:
191+
from apps.wallets.wallet import Wallet as _Wallet
192+
except ModuleNotFoundError as exc:
193+
if exc.name == "embit":
194+
raise ModuleNotFoundError(
195+
"Native test suite requires the 'embit' package. "
196+
"Install it with 'pip install -r test/integration/requirements.txt'."
197+
) from exc
198+
raise
199+
200+
if not hasattr(_Wallet, '_native_original_from_descriptor'):
201+
_Wallet._native_original_from_descriptor = _Wallet.from_descriptor
202+
203+
def _native_from_descriptor(cls, desc: str, path):
204+
desc = desc.split('#')[0].replace(' ', '')
205+
descriptor = cls.DescriptorClass.from_string(desc)
206+
return cls(descriptor, path)
207+
208+
_Wallet.from_descriptor = classmethod(_native_from_descriptor)

test/run_native_tests.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import sys
2+
from pathlib import Path
3+
4+
ROOT = Path(__file__).resolve().parent
5+
sys.path.insert(0, str((ROOT / "../src").resolve()))
6+
sys.path.insert(0, str((ROOT / "../f469-disco/libs/common").resolve()))
7+
sys.path.insert(0, str((ROOT / "../f469-disco/libs/unix").resolve()))
8+
sys.path.insert(0, str((ROOT / "../f469-disco/usermods/udisplay_f469/display_unixport").resolve()))
9+
sys.path.insert(0, str((ROOT / "../f469-disco/tests").resolve()))
10+
11+
from native_support import setup_native_stubs
12+
13+
setup_native_stubs()
14+
15+
import unittest
16+
from tests import util
17+
18+
util.clear_testdir()
19+
unittest.main('tests_native', verbosity=2)

0 commit comments

Comments
 (0)