improved QR reading, BBQr tests
This commit is contained in:
parent
10e9a4f929
commit
4e1b0f0f69
@ -1,6 +1,6 @@
|
||||
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json
|
||||
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, pdb
|
||||
from subprocess import check_output
|
||||
from ckcc.protocol import CCProtocolPacker
|
||||
from helpers import B2A, U2SAT
|
||||
@ -438,8 +438,14 @@ def cap_image(sim_exec, is_q1):
|
||||
fn = os.path.realpath(f'./debug/snap-{random.randint(1E6, 9E6)}.png')
|
||||
try:
|
||||
sim_exec(f"from glob import dis; dis.dis.save_snapshot({fn!r})")
|
||||
time.sleep(0.250) # need this but I don't see why
|
||||
rv = Image.open(fn)
|
||||
while 1:
|
||||
time.sleep(0.010)
|
||||
try:
|
||||
rv = Image.open(fn)
|
||||
break
|
||||
except:
|
||||
# PIL parsing errors and FileNotFoundError
|
||||
continue
|
||||
finally:
|
||||
os.remove(fn)
|
||||
return rv
|
||||
@ -514,7 +520,7 @@ def qr_quality_check():
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def cap_screen_qr(cap_image):
|
||||
def doit():
|
||||
def doit(no_history=False):
|
||||
# NOTE: version=4 QR is pixel doubled to be 66x66 with 2 missing lines at bottom
|
||||
# LATER: not doing that anymore; v=3 doubled, all higher 1:1 pixels (tiny)
|
||||
global QR_HISTORY
|
||||
@ -530,9 +536,10 @@ def cap_screen_qr(cap_image):
|
||||
|
||||
orig_img = cap_image()
|
||||
|
||||
# document it
|
||||
tname = os.environ.get('PYTEST_CURRENT_TEST')
|
||||
QR_HISTORY.append( (tname, orig_img) )
|
||||
if not no_history:
|
||||
# document it
|
||||
tname = os.environ.get('PYTEST_CURRENT_TEST')
|
||||
QR_HISTORY.append( (tname, orig_img) )
|
||||
|
||||
if orig_img.width == 128:
|
||||
# Mk3/4 - pull out just the QR, blow it up 16x
|
||||
@ -543,18 +550,32 @@ def cap_screen_qr(cap_image):
|
||||
else:
|
||||
# Q1 - trim status line, convert to greyscale
|
||||
w, h = orig_img.size # 320x240
|
||||
img = orig_img.convert('L')
|
||||
# - remove status bar (harmless)
|
||||
# - and progress bar (does cause readability issues)
|
||||
# - MAYBE: blow up the size, helps on fine QR.
|
||||
# - but some are still unreadable!?!?
|
||||
img = orig_img.crop( (0, 15, w, h-5) ).convert('L')
|
||||
#w, h = img.size
|
||||
#img = img.resize( (w*9, h*9))
|
||||
|
||||
img.save('debug/last-qr.png')
|
||||
#img.show()
|
||||
|
||||
# Important: w/h reversed in shape of NP array
|
||||
np = numpy.array(img.getdata(), 'uint8').reshape(img.height, img.width)
|
||||
# Above usually works @ zoom=1, but not always!
|
||||
# - simulate what users do... move phone back and forth until it scans
|
||||
oo = img
|
||||
for zoom in range(1, 7):
|
||||
if zoom > 1:
|
||||
w, h = oo.size
|
||||
img = oo.resize( (w*zoom, h*zoom) )
|
||||
|
||||
scanner = zbar.Scanner()
|
||||
for sym, value, *_ in scanner.scan(np):
|
||||
assert sym == 'QR-Code', 'unexpected symbology: ' + sym
|
||||
return value # bytes, could be binary
|
||||
# Important: w/h reversed in shape of NP array
|
||||
np = numpy.array(img.getdata(), 'uint8').reshape(img.height, img.width)
|
||||
|
||||
scanner = zbar.Scanner()
|
||||
for sym, value, *_ in scanner.scan(np):
|
||||
assert sym == 'QR-Code', 'unexpected symbology: ' + sym
|
||||
return value # bytes, could be binary
|
||||
|
||||
# for debug, check debug/last-qr.png
|
||||
raise RuntimeError('qr code not found')
|
||||
@ -1062,7 +1083,7 @@ def decode_with_bitcoind(bitcoind):
|
||||
return bitcoind.rpc.decoderawtransaction(B2A(raw_txn))
|
||||
except ConnectionResetError:
|
||||
# bitcoind sleeps on us sometimes, give it another chance.
|
||||
return bitcoind.decoderawtransaction(B2A(raw_txn))
|
||||
return bitcoind.rpc.decoderawtransaction(B2A(raw_txn))
|
||||
|
||||
return doit
|
||||
|
||||
@ -1074,10 +1095,10 @@ def decode_psbt_with_bitcoind(bitcoind):
|
||||
from base64 import b64encode
|
||||
|
||||
try:
|
||||
return bitcoind.decodepsbt(b64encode(raw_psbt).decode('ascii'))
|
||||
return bitcoind.rpc.decodepsbt(b64encode(raw_psbt).decode('ascii'))
|
||||
except ConnectionResetError:
|
||||
# bitcoind sleeps on us sometimes, give it another chance.
|
||||
return bitcoind.decodepsbt(b64encode(raw_psbt).decode('ascii'))
|
||||
return bitcoind.rpc.decodepsbt(b64encode(raw_psbt).decode('ascii'))
|
||||
|
||||
return doit
|
||||
|
||||
@ -1531,6 +1552,20 @@ def nfc_write(request, needs_nfc):
|
||||
except:
|
||||
return doit_usb
|
||||
|
||||
@pytest.fixture()
|
||||
def scan_a_qr(sim_exec, is_q1):
|
||||
if not is_q1:
|
||||
raise pytest.xfail('needs scanner')
|
||||
|
||||
def doit(qr):
|
||||
assert isinstance(qr, str)
|
||||
qr = qr.encode('ascii')
|
||||
rv = sim_exec(f'glob.SCAN._q.put_nowait({qr!r})')
|
||||
if 'Traceback' in rv: raise pytest.fail(rv)
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
def ccfile_wrap(recs):
|
||||
from struct import pack
|
||||
CC_FILE = bytes([0xE2, 0x43, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00, 0x03])
|
||||
|
||||
@ -20,3 +20,6 @@ pyscard==2.0.2
|
||||
# BSMS library
|
||||
git+https://github.com/coinkite/bsms-bitcoin-secure-multisig-setup.git@master#egg=bsms-bitcoin-secure-multisig-setup
|
||||
|
||||
# BBQr library
|
||||
git+https://github.com/coinkite/BBQr.git@master#egg=bbqr
|
||||
|
||||
|
||||
@ -3,20 +3,79 @@
|
||||
# BBQr and secure notes.
|
||||
#
|
||||
|
||||
import pytest, os, time
|
||||
import pytest, os, time, random
|
||||
from helpers import B2A, prandom
|
||||
from binascii import b2a_hex, a2b_hex
|
||||
from bbqr import split_qrs, join_qrs
|
||||
from charcodes import KEY_QR
|
||||
|
||||
@pytest.mark.parametrize('size', [ 20, 990] )
|
||||
def XXX_test_show_bbqr_codes(size, sim_execfile, need_keypress, cap_screen_qr, sim_exec):
|
||||
sim_exec('import main; main.BBQR_SIZE = %r; ' % size)
|
||||
rv = sim_execfile('devtest/bbqr.py')
|
||||
assert 'Error' not in rv
|
||||
readback = cap_screen_qr()
|
||||
assert readback == 'sdf'
|
||||
# All tests in this file are exclusively meant for Q
|
||||
#
|
||||
@pytest.fixture(autouse=True)
|
||||
def THIS_FILE_requires_q1(is_q1):
|
||||
if not is_q1:
|
||||
raise pytest.skip('Q1 only')
|
||||
|
||||
@pytest.fixture
|
||||
def readback_bbqr(need_keypress, cap_screen_qr, sim_exec):
|
||||
def doit():
|
||||
num_parts = None
|
||||
encoding, file_type = None, None
|
||||
parts = {}
|
||||
|
||||
for retries in range(1000):
|
||||
#time.sleep(0.05) # not really sync'ed
|
||||
try:
|
||||
rb = cap_screen_qr(no_history=True).decode('ascii')
|
||||
except RuntimeError:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
#print(rb[0:20]+'...')
|
||||
|
||||
if len(rb) > 2 and rb[0:2] != 'B$':
|
||||
# it sent a non-BBQr QR which isn't wrong.. but let caller decode
|
||||
return 0, None, None, rb
|
||||
|
||||
assert rb[0:2] == 'B$'
|
||||
if not encoding:
|
||||
encoding = rb[2]
|
||||
else:
|
||||
assert encoding == rb[2]
|
||||
|
||||
if not file_type:
|
||||
file_type = rb[3]
|
||||
else:
|
||||
assert file_type == rb[3]
|
||||
|
||||
if num_parts is None:
|
||||
num_parts = int(rb[4:6], 36)
|
||||
assert num_parts >= 1
|
||||
else:
|
||||
assert num_parts == int(rb[4:6], 36)
|
||||
|
||||
part = int(rb[6:8], 36)
|
||||
assert part < num_parts
|
||||
|
||||
if part in parts:
|
||||
assert parts[part] == rb
|
||||
else:
|
||||
parts[part] = rb
|
||||
|
||||
if len(parts) >= num_parts:
|
||||
break
|
||||
|
||||
if len(parts) != num_parts:
|
||||
# timed out
|
||||
raise pytest.fail(f'Could not read all parts of BBQr: '\
|
||||
f'got {[parts.keys()]} of {num_parts}')
|
||||
|
||||
return num_parts, encoding, file_type, parts
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture
|
||||
def render_bbqr(need_keypress, cap_screen_qr, sim_exec):
|
||||
def render_bbqr(need_keypress, cap_screen_qr, sim_exec, readback_bbqr):
|
||||
def doit(data=None, str_expr=None, file_type='B', msg=None, setup=''):
|
||||
assert data or str_expr
|
||||
|
||||
@ -35,47 +94,12 @@ def render_bbqr(need_keypress, cap_screen_qr, sim_exec):
|
||||
print(f"RESP: {resp}")
|
||||
assert 'error' not in resp.lower()
|
||||
|
||||
num_parts = None
|
||||
encoding = None
|
||||
parts = {}
|
||||
for retries in range(1000):
|
||||
time.sleep(0.005) # not really sync'ed
|
||||
try:
|
||||
rb = cap_screen_qr().decode('ascii')
|
||||
except RuntimeError:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
print(rb[0:20])
|
||||
assert rb[0:2] == 'B$'
|
||||
if not encoding:
|
||||
encoding = rb[2]
|
||||
else:
|
||||
assert encoding == rb[2]
|
||||
assert rb[3] == file_type
|
||||
|
||||
if num_parts is None:
|
||||
num_parts = int(rb[4:6], 36)
|
||||
assert num_parts >= 1
|
||||
else:
|
||||
assert num_parts == int(rb[4:6], 36)
|
||||
part = int(rb[6:8], 36)
|
||||
assert part < num_parts
|
||||
|
||||
parts[part] = rb
|
||||
|
||||
if len(parts) >= num_parts:
|
||||
break
|
||||
num_parts, encoding, rb_ft, parts = readback_bbqr()
|
||||
assert rb_ft == file_type
|
||||
|
||||
print(sim_exec(f'import main; main.TT.cancel()'))
|
||||
|
||||
need_keypress('\r')
|
||||
need_keypress('0') # for menu to redraw
|
||||
|
||||
if len(parts) != num_parts:
|
||||
# timed out
|
||||
raise pytest.fail(f'Could not read all parts of BBQr: got {[parts.keys()]} of {num_parts}')
|
||||
|
||||
# we only can decode simple BBQr here
|
||||
assert encoding == 'H'
|
||||
body = a2b_hex(''.join(p[8:] for p in [parts[i] for i in range(num_parts)]))
|
||||
@ -97,22 +121,107 @@ def test_show_bbqr_sizes(size, need_keypress, cap_screen_qr, sim_exec, render_bb
|
||||
assert len(data) == size
|
||||
assert data == 'a' * size
|
||||
|
||||
@pytest.mark.parametrize('src', [ 'rng', 'gpu'] )
|
||||
ft, data2 = join_qrs(parts.values())
|
||||
assert data2.decode('utf-8') == data
|
||||
assert ft == 'U'
|
||||
|
||||
@pytest.mark.parametrize('src', [ 'rng', 'gpu', 'bigger'] )
|
||||
def test_show_bbqr_contents(src, need_keypress, cap_screen_qr, sim_exec, render_bbqr, load_shared_mod):
|
||||
|
||||
args = dict(msg=f'Test {src}', file_type='B')
|
||||
if src == 'rng':
|
||||
args['data'] = expect = prandom(500) # limited by simulated USB path
|
||||
elif src == 'gpu':
|
||||
elif src in { 'gpu', 'bigger' }:
|
||||
args['setup'] = 'from gpu_binary import BINARY'
|
||||
args['str_expr'] = '"BINARY"'
|
||||
cc_gpu_bin = load_shared_mod('cc_gpu_bin', '../shared/gpu_binary.py')
|
||||
expect = cc_gpu_bin.BINARY
|
||||
if src == 'gpu':
|
||||
args['str_expr'] = 'BINARY'
|
||||
expect = cc_gpu_bin.BINARY
|
||||
elif src == 'bigger':
|
||||
args['str_expr'] = 'BINARY*10'
|
||||
expect = cc_gpu_bin.BINARY*10
|
||||
|
||||
data, parts = render_bbqr(**args)
|
||||
|
||||
assert len(parts) > 1
|
||||
assert len(data) == len(expect)
|
||||
assert data == expect
|
||||
ft, data2 = join_qrs(parts.values())
|
||||
assert data2 == data
|
||||
assert ft == 'B'
|
||||
|
||||
@pytest.mark.parametrize('size', [ 10 ] )
|
||||
@pytest.mark.parametrize('max_ver', [ 10, 20 ] )
|
||||
@pytest.mark.parametrize('encoding', '2HZ' )
|
||||
@pytest.mark.parametrize('partial', [False, True])
|
||||
def test_bbqr_psbt(size, encoding, max_ver, partial,
|
||||
need_keypress, scan_a_qr, readback_bbqr,
|
||||
cap_screen_qr, render_bbqr, goto_home, use_regtest, decode_psbt_with_bitcoind,
|
||||
decode_with_bitcoind, fake_txn, dev, cap_story, start_sign, end_sign):
|
||||
|
||||
num_in = size
|
||||
num_out = size*10
|
||||
|
||||
def hack(psbt):
|
||||
if partial:
|
||||
# change first input to not be ours
|
||||
pk = list(psbt.inputs[0].bip32_paths.keys())[0]
|
||||
pp = psbt.inputs[0].bip32_paths[pk]
|
||||
psbt.inputs[0].bip32_paths[pk] = b'what' + pp[4:]
|
||||
|
||||
psbt = fake_txn(num_in, num_out, dev.master_xpub, psbt_hacker=hack)
|
||||
open('debug/last.psbt', 'wb').write(psbt)
|
||||
|
||||
goto_home()
|
||||
need_keypress(KEY_QR)
|
||||
|
||||
actual_vers, parts = split_qrs(psbt, 'P', max_version=max_ver, encoding=encoding)
|
||||
# def split_qrs(raw, type_code, encoding=None,
|
||||
# min_split=1, max_split=1295, min_version=5, max_version=40
|
||||
|
||||
random.shuffle(parts)
|
||||
|
||||
for p in parts:
|
||||
scan_a_qr(p)
|
||||
time.sleep(4.0 / len(parts)) # just so we can watch
|
||||
|
||||
for r in range(20):
|
||||
title, story = cap_story()
|
||||
if 'OK TO SEND' in title:
|
||||
break
|
||||
time.sleep(.1)
|
||||
else:
|
||||
raise pytest.fail('never saw it?')
|
||||
|
||||
# approve it
|
||||
need_keypress('y')
|
||||
|
||||
time.sleep(.2)
|
||||
|
||||
# expect signed txn back
|
||||
num_parts, encoding, file_type, parts = readback_bbqr()
|
||||
if num_parts == 0:
|
||||
# not sent as BBQr .. assume Hex
|
||||
rb = a2b_hex(parts)
|
||||
file_type = 'P' if rb[0:4] == b'psbt' else 'T'
|
||||
else:
|
||||
assert file_type in 'TP'
|
||||
_, rb = join_qrs(parts.values())
|
||||
|
||||
if file_type == 'T':
|
||||
assert not partial
|
||||
decoded = decode_with_bitcoind(rb)
|
||||
elif file_type == 'P':
|
||||
assert partial
|
||||
assert rb[0:4] == b'psbt'
|
||||
decoded = decode_psbt_with_bitcoind(rb)
|
||||
assert not decoded['unknown']
|
||||
decoded = decoded['tx']
|
||||
|
||||
# just smoke test; syntax not content
|
||||
assert len(decoded['vin']) == num_in
|
||||
assert len(decoded['vout']) == num_out
|
||||
|
||||
need_keypress('x') # back to menu
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user