merging main upstream repo into fork

This commit is contained in:
BamaHodl
2024-06-21 12:53:42 -05:00
36 changed files with 999 additions and 603 deletions
+53 -27
View File
@@ -1,12 +1,30 @@
name: Build name: Build
on: on:
pull_request:
# Build on changes to this workflow files in PRs to test proposed changes
paths:
- '.github/workflows/build.yml'
push:
branches:
- main
- dev
workflow_dispatch: workflow_dispatch:
inputs:
os-ref:
description: The seedsigner-os ref (tag/branch/sha1) to use
default: main
required: true
# Increment this number as part of a PR to trigger an image build for the PR
# trigger = 0
jobs: jobs:
build: build:
name: build name: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Prevent resource consuming cron triggered runs in forks
if: (!github.event.repository.fork || github.event_name == 'workflow_dispatch')
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -16,25 +34,41 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
repository: "seedsigner/seedsigner-os" repository: "seedsigner/seedsigner-os"
# use the os-ref input parameter in case of workflow_dispatch or default to main in case of cron triggers
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.os-ref || 'main' }}
submodules: true submodules: true
path: "seedsigner-os" path: "seedsigner-os"
# get full history + tags for "git describe"
- name: get seedsigner-os latest commit hash fetch-depth: 0
id: get-seedsigner-os-hash
run: |
cd seedsigner-os
echo "builder_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: checkout source - name: checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
# ref defaults to repo default-branch=dev (cron) or SHA of event (workflow_dispatch)
path: "seedsigner-os/opt/rootfs-overlay/opt" path: "seedsigner-os/opt/rootfs-overlay/opt"
# get full history + tags for "git describe"
fetch-depth: 0
- name: get seedsigner latest commit hash - name: Get and set meta data
id: get-seedsigner-hash
run: | run: |
git init # The builder_hash (seedsigner-os hash) for the cache action step key
echo "source_hash=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV echo "builder_hash=$(git -C seedsigner-os rev-parse --short HEAD)"| tee -a $GITHUB_ENV
# Derive tag based versions, like 0.7.0-40-g0424967 (=$tag-$number-of-commits-since-tag-$short-sha1),
# or just e.g. 0.7.0, if we are exactly on a 0.7.0 tagged commit.
# --always to fall back to commit sha, if no tag present like in partial forks of the repo
os_version="$(git -C seedsigner-os describe --tags --always)"
source_version="$(git -C seedsigner-os/opt/rootfs-overlay/opt describe --tags --always)"
# Combine seedsigner and seedsigner-os version into one version string and squash the versions, if
# they are identical: So os_version=0.7.0 + source_version=0.7.0 combine to just only "0.7.0",
# whereas os_version=0.6.0-61-g9fafebe + source_version=0.7.0-40-g0424967 combine to "os0.6.0-61-g9fafebe_sw0.7.0-40-g0424967"
if [ "${os_version}" = "${source_version}" ]; then
# seedsigner + seedsigner_os have the same tag
echo "img_version=${source_version}"| tee -a $GITHUB_ENV
else
echo "img_version=os${os_version}_sw${source_version}"| tee -a $GITHUB_ENV
fi
- name: delete unnecessary files - name: delete unnecessary files
run: | run: |
@@ -44,11 +78,9 @@ jobs:
ls -la src ls -la src
- name: restore build cache - name: restore build cache
id: build-cache-restore uses: actions/cache@v3
uses: actions/cache/restore@v3 # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour,
# Caching seedsigner-os/buildroot_dl is optional. # while consuming ~850 MB storage space).
# Caching it can save a small amount of build time,
# but it will occupy a larger amount of storage space.
with: with:
path: | path: |
~/.buildroot-ccache/ ~/.buildroot-ccache/
@@ -62,16 +94,6 @@ jobs:
cd seedsigner-os/opt cd seedsigner-os/opt
./build.sh --${{ matrix.target }} --skip-repo --no-clean ./build.sh --${{ matrix.target }} --skip-repo --no-clean
- name: save build cache
id: build-cache-save
if: steps.build-cache-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v3
with:
path: |
~/.buildroot-ccache/
seedsigner-os/buildroot_dl
key: build-cache-${{ matrix.target }}-${{ env.builder_hash }}
- name: list image (before rename) - name: list image (before rename)
run: | run: |
ls -la seedsigner-os/images ls -la seedsigner-os/images
@@ -79,7 +101,7 @@ jobs:
- name: rename image - name: rename image
run: | run: |
cd seedsigner-os/images cd seedsigner-os/images
mv seedsigner_os*.img seedsigner_os.${{ env.source_hash }}.${{ matrix.target }}.img mv seedsigner_os*.img seedsigner_os.${{ env.img_version }}.${{ matrix.target }}.img
- name: print sha256sum - name: print sha256sum
run: | run: |
@@ -96,6 +118,8 @@ jobs:
name: seedsigner_os_images name: seedsigner_os_images
path: "seedsigner-os/images/*.img" path: "seedsigner-os/images/*.img"
if-no-files-found: error if-no-files-found: error
# maximum 90 days retention
retention-days: 90
sha256sum: sha256sum:
name: calculate sha256sum name: calculate sha256sum
@@ -128,4 +152,6 @@ jobs:
with: with:
name: seedsigner_os_images name: seedsigner_os_images
path: "images/*.sha256" path: "images/*.sha256"
if-no-files-found: error if-no-files-found: error
# maximum 90 days retention
retention-days: 90
+16 -5
View File
@@ -10,13 +10,17 @@
* [Verifying the Software](#verifying-the-software) * [Verifying the Software](#verifying-the-software)
* [Enclosure Designs](#enclosure-designs) * [Enclosure Designs](#enclosure-designs)
* [SeedQR Printable Templates](#seedqr-printable-templates) * [SeedQR Printable Templates](#seedqr-printable-templates)
* [Manual Installation Instructions](#manual-installation-instructions) * [Build from Source](#build-from-source)
* [Developer Local Build Instructions](#developer-local-build-instructions)
--------------- ---------------
# Project Summary # Project Summary
[![CI](https://github.com/SeedSigner/seedsigner/actions/workflows/tests.yml/badge.svg)](https://github.com/SeedSigner/seedsigner/actions/workflows/tests.yml)
[![Build](https://github.com/SeedSigner/seedsigner/actions/workflows/build.yml/badge.svg)](https://github.com/SeedSigner/seedsigner/actions/workflows/build.yml)
The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multisignature (aka "multisig") wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model. The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multisignature (aka "multisig") wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model.
Additional information about the project can be found at [SeedSigner.com](https://seedsigner.com). Additional information about the project can be found at [SeedSigner.com](https://seedsigner.com).
@@ -81,9 +85,11 @@ Notes:
# Software Installation # Software Installation
## A Special Note On Minimizing Trust ## A Special Note On Minimizing Trust
As is the nature of pre-packaged software downloads, downloading and using the prepared SeedSigner release images means implicitly placing trust in the individual preparing those images; in our project the release images are prepared and signed by the eponymous creator of the project, SeedSigner "the person". That individual is additionally the only person in possession of the PGP keys that are used to sign the release images. As is the nature of pre-packaged software downloads, downloading and using the prepared SeedSigner release images means implicitly placing trust in the people preparing those images; in our project the released images are prepared and signed by the eponymous creator of the project, SeedSigner "the person". That individual is additionally the only person in possession of the PGP keys that are used to sign the release images.
However, one of the many advantages of the open source software model is that the need for this kind of trust can be negated by our users' ability to (1) review the project's source code and (2) assemble the operating image necessary to use the software themselves. From our project's inception, instructions to build a SeedSigner operating image (using precisely the same process that is used to create the prepared release images) have been made available. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](docs/manual_installation.md). Starting with v0.7.0, the images distributed via GitHub are reproducible. This means you and others can verify the released images are byte-for-byte the same when built from source. You can contribute to this project by building from source and sharing the hash of the final images.
Instructions to build a SeedSigner OS image (using precisely the same process that is used to create the prepared release images) have been made available. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](https://github.com/SeedSigner/seedsigner-os/blob/main/docs/building.md).
## Downloading the Software ## Downloading the Software
@@ -118,6 +124,8 @@ Once the files have all finished downloading, follow the steps below to verify t
You can quickly verify that the software you just downloaded is both authentic and unaltered, by following these instructions. You can quickly verify that the software you just downloaded is both authentic and unaltered, by following these instructions.
We assume you are running the commands from a computer where both [GPG](https://gnupg.org/download/index.html) and [shasum](https://command-not-found.com/shasum) are already installed, and that you also know [how to navigate on a terminal](https://terminalcheatsheet.com/guides/navigate-terminal). We assume you are running the commands from a computer where both [GPG](https://gnupg.org/download/index.html) and [shasum](https://command-not-found.com/shasum) are already installed, and that you also know [how to navigate on a terminal](https://terminalcheatsheet.com/guides/navigate-terminal).
> You must run the following verification before opening or mounting the .img file.
> Some operating systems modify the file on mount causing verification to fail.
### Step 1. Verify that the signature (.sig) file is genuine: ### Step 1. Verify that the signature (.sig) file is genuine:
@@ -325,5 +333,8 @@ Letter templates(8.5in * 11in):
* [29x29 - stores 24-word seeds ONLY as plaintext SeedQR format ONLY](docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf) * [29x29 - stores 24-word seeds ONLY as plaintext SeedQR format ONLY](docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf)
--------------- ---------------
# Manual Installation Instructions # Build from Source
see the docs: [Manual Installation Instructions](docs/manual_installation.md) See the [SeedSigner OS repo](https://github.com/SeedSigner/seedsigner-os/) for instructions.
# Developer Local Build Instructions
Raspberry Pi OS is commonly used for development. See the [Raspberry Pi OS Build Instructions](docs/raspberry_pi_os_build_instructions.md)
+67
View File
@@ -205,3 +205,70 @@ We double-checked in two different web tools implementing different methods for
So congratulations if the fingerprints, zpubs and addresses all match up in your example so you can be much more confident that nothing is wrong with your generated seed. So congratulations if the fingerprints, zpubs and addresses all match up in your example so you can be much more confident that nothing is wrong with your generated seed.
---
# Command Line Tool
_(for more advanced/python-savvy users)_
Run the exact same SeedSigner mnemonic generation code from the command line to quickly test and externally verify the results.
Create a python virtualenv (out of the scope of this doc) and install dependencies:
```bash
pip3 install embit
# Install the main project code to make it importable
pip3 install -e .
```
Then run the utility script with `-h` to view the usage instructions:
```bash
cd tools
python3 mnemonic.py -h
```
```
Verify SeedSigner's dice rolls and coin flip entropy-to-mnemonic conversion via this tool.
Compare its results against iancoleman.io/bip39 and bitcoiner.guide/seed
Usage:
# 50 dice rolls / 12-word mnemonic
python3 mnemonic.py dice 5624433434...
# 99 dice rolls / 24-word mnemonic
python3 mnemonic.py dice 6151463561...
# 50 dice rolls, entered as 0-5 / 12-word mnemonic
python3 mnemonic.py --zero-indexed-dice dice 5135535514...
# 128 coin flips / 12-word mnemonic
python3 mnemonic.py coins 1111100111...
# 256 coin flips / 24-word mnemonic
python mnemonic.py coins 0010111010...
# GENERATE 50 random dice rolls / 12-word mnemonic
python3 mnemonic.py dice rand12
# GENERATE 99 random dice rolls / 24-word mnemonic
python3 mnemonic.py dice rand24
# GENERATE 99 random dice rolls, entered as 0-5 / 24-word mnemonic
python3 mnemonic.py --zero-indexed-dice dice rand24
# GENERATE 128 random coin flips / 12-word mnemonic
python3 mnemonic.py coins rand12
# GENERATE 256 random coin flips / 24-word mnemonic
python3 mnemonic.py coins rand24
```
### How to get the same results in iancoleman.io
Always specify your expected length in the "Mnemonic Length" droplist (defaults to "Use Raw Entropy (3 words per 32 bits)").
Dice Rolls: Do NOT use the "Dice [1-6]" option; select "Base 10 [0-9]" or "Hex [0-9A-F]"
Zero-indexed dice rolls: Select "Base 6 [0-5]", "Base 10 [0-9]", or "Hex [0-9A-F]"
Coin Flips: Select "Binary [0-1]", "Base 6 [0-5]", "Base 10 [0-9]", or "Hex [0-9A-F]"
@@ -1,4 +1,6 @@
# Manual Installation Instructions # Raspberry Pi OS Local Dev Build Instructions
Since v0.6.0, official releases use our custom [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/) However, project contributors looking to do rapid development cycles typically use the older Raspberry Pi OS that we had previously built on prior to v0.6.0. If you're here to set up your SeedSigner for local development, continue reading.
Begin by acquiring the latest 32-bit, Buster-based Raspberry Pi Lite operating system. This guide was tested using the version dated 2023-05-03; which can be found here: Begin by acquiring the latest 32-bit, Buster-based Raspberry Pi Lite operating system. This guide was tested using the version dated 2023-05-03; which can be found here:
@@ -109,7 +111,7 @@ sudo apt install python3-apt -y
Copy this entire box and run it as one command (~15 minutes on a Pi Zero 1.3): Copy this entire box and run it as one command (~15 minutes on a Pi Zero 1.3):
```bash ```bash
sudo apt update && sudo apt install -y wiringpi python3-pip \ sudo apt update && sudo apt install -y wiringpi python3-pip \
python3-numpy python-pil libjpeg-dev zlib1g-dev libopenjp2-7 \ python-pil libjpeg-dev zlib1g-dev libopenjp2-7 \
git python3-opencv python3-picamera libatlas-base-dev qrencode git python3-opencv python3-picamera libatlas-base-dev qrencode
``` ```
+2
View File
@@ -1,3 +1,5 @@
picamera==1.13 picamera==1.13
# numpy = transitive picamera dependency
numpy==1.25.2
RPi.GPIO==0.7.0 RPi.GPIO==0.7.0
spidev==3.5 spidev==3.5
-2
View File
@@ -1,7 +1,5 @@
embit==0.7.0 embit==0.7.0
numpy==1.25.2
Pillow==9.4.0 Pillow==9.4.0
pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03 pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03
qrcode==7.3.1 qrcode==7.3.1
six==1.16.0
urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a
+9
View File
@@ -398,6 +398,15 @@ class Controller(Singleton):
# Start the screensaver, but it will block until it can acquire the Renderer.lock. # Start the screensaver, but it will block until it can acquire the Renderer.lock.
self.screensaver.start() self.screensaver.start()
print("Controller: Screensaver started") print("Controller: Screensaver started")
def reset_screensaver_timeout(self):
"""
Reset the screensaver's timeout starting point to right now (i.e. make it think
that zero time has elapsed since the last user interaction).
"""
from seedsigner.hardware.buttons import HardwareButtons
HardwareButtons.get_instance().update_last_input_time()
def activate_toast(self, toast_manager_thread: BaseToastOverlayManagerThread): def activate_toast(self, toast_manager_thread: BaseToastOverlayManagerThread):
+2
View File
@@ -29,6 +29,8 @@ class GUIConstants:
BITCOIN_ORANGE = "#FF9416" BITCOIN_ORANGE = "#FF9416"
TESTNET_COLOR = "#00F100" TESTNET_COLOR = "#00F100"
REGTEST_COLOR = "#00CAF1" REGTEST_COLOR = "#00CAF1"
GREEN_INDICATOR_COLOR = "#00FF00"
INACTIVE_COLOR = "#414141"
ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900" ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900"
ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons" ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons"
+122 -14
View File
@@ -7,10 +7,10 @@ from seedsigner.gui import renderer
from seedsigner.hardware.buttons import HardwareButtonsConstants from seedsigner.hardware.buttons import HardwareButtonsConstants
from seedsigner.hardware.camera import Camera from seedsigner.hardware.camera import Camera
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
from seedsigner.models.threads import BaseThread from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from .screen import BaseScreen, ButtonListScreen from .screen import BaseScreen
from ..components import GUIConstants, Fonts, TextArea from ..components import GUIConstants, Fonts, SeedSignerIconConstants
@@ -47,6 +47,9 @@ class ScanScreen(BaseScreen):
framerate: int = 6 # TODO: alternate optimization for Pi Zero 2W? framerate: int = 6 # TODO: alternate optimization for Pi Zero 2W?
render_rect: tuple[int,int,int,int] = None render_rect: tuple[int,int,int,int] = None
FRAME__ADDED_PART = 1
FRAME__REPEATED_PART = 2
FRAME__MISS = 3
def __post_init__(self): def __post_init__(self):
from seedsigner.hardware.camera import Camera from seedsigner.hardware.camera import Camera
@@ -58,17 +61,22 @@ class ScanScreen(BaseScreen):
self.camera = Camera.get_instance() self.camera = Camera.get_instance()
self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb") self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb")
self.frames_decode_status = ThreadsafeCounter()
self.frames_decoded_counter = ThreadsafeCounter()
self.threads.append(ScanScreen.LivePreviewThread( self.threads.append(ScanScreen.LivePreviewThread(
camera=self.camera, camera=self.camera,
decoder=self.decoder, decoder=self.decoder,
renderer=self.renderer, renderer=self.renderer,
instructions_text=self.instructions_text, instructions_text=self.instructions_text,
render_rect=self.render_rect, render_rect=self.render_rect,
frame_decode_status=self.frames_decode_status,
frames_decoded_counter=self.frames_decoded_counter,
)) ))
class LivePreviewThread(BaseThread): class LivePreviewThread(BaseThread):
def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int]): def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int], frame_decode_status: ThreadsafeCounter, frames_decoded_counter: ThreadsafeCounter):
self.camera = camera self.camera = camera
self.decoder = decoder self.decoder = decoder
self.renderer = renderer self.renderer = renderer
@@ -77,6 +85,9 @@ class ScanScreen(BaseScreen):
self.render_rect = render_rect self.render_rect = render_rect
else: else:
self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height) self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height)
self.frame_decode_status = frame_decode_status
self.frames_decoded_counter = frames_decoded_counter
self.last_frame_decoded_count = self.frames_decoded_counter.cur_count
self.render_width = self.render_rect[2] - self.render_rect[0] self.render_width = self.render_rect[2] - self.render_rect[0]
self.render_height = self.render_rect[3] - self.render_rect[1] self.render_height = self.render_rect[3] - self.render_rect[1]
self.decoder_fps = "0.0" self.decoder_fps = "0.0"
@@ -85,12 +96,15 @@ class ScanScreen(BaseScreen):
def run(self): def run(self):
from timeit import default_timer as timer
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE) instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
# pre-calculate how big the animated QR percent display can be
left, _, right, _ = instructions_font.getbbox("100%")
progress_text_width = right - left
start_time = time.time() start_time = time.time()
num_frames = 0 num_frames = 0
debug = False
show_framerate = False # enable for debugging / testing show_framerate = False # enable for debugging / testing
while self.keep_running: while self.keep_running:
frame = self.camera.read_video_stream(as_image=True) frame = self.camera.read_video_stream(as_image=True)
@@ -98,16 +112,22 @@ class ScanScreen(BaseScreen):
num_frames += 1 num_frames += 1
cur_time = time.time() cur_time = time.time()
cur_fps = num_frames / (cur_time - start_time) cur_fps = num_frames / (cur_time - start_time)
if self.decoder and self.decoder.get_percent_complete() > 0 and self.decoder.is_psbt:
scan_text = str(self.decoder.get_percent_complete()) + "% Complete" scan_text = None
if show_framerate: progress_percentage = self.decoder.get_percent_complete()
scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}" if progress_percentage == 0:
else: # We've just started scanning, no results yet
if show_framerate: if show_framerate:
scan_text = f"{cur_fps:0.2f} | {self.decoder_fps}" scan_text = f"{cur_fps:0.2f} | {self.decoder_fps}"
else: else:
scan_text = self.instructions_text scan_text = self.instructions_text
elif debug:
# Special debugging output for animated QRs
scan_text = f"{self.decoder.get_percent_complete()}% | {self.decoder.get_percent_complete(weight_mixed_frames=True)}% (new)"
if show_framerate:
scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}"
with self.renderer.lock: with self.renderer.lock:
if frame.width > self.render_width or frame.height > self.render_height: if frame.width > self.render_width or frame.height > self.render_height:
frame = frame.resize( frame = frame.resize(
@@ -115,14 +135,13 @@ class ScanScreen(BaseScreen):
resample=Image.NEAREST # Use nearest neighbor for max speed resample=Image.NEAREST # Use nearest neighbor for max speed
) )
draw = ImageDraw.Draw(frame)
if scan_text: if scan_text:
# Note: shadowed text (adding a 'stroke' outline) can # Note: shadowed text (adding a 'stroke' outline) can
# significantly slow down the rendering. # significantly slow down the rendering.
# Temp solution: render a slight 1px shadow behind the text # Temp solution: render a slight 1px shadow behind the text
# TODO: Replace the instructions_text with a disappearing # TODO: Replace the instructions_text with a disappearing
# toast/popup (see: QR Brightness UI)? # toast/popup (see: QR Brightness UI)?
draw = ImageDraw.Draw(frame)
draw.text(xy=( draw.text(xy=(
int(self.renderer.canvas_width/2 + 2), int(self.renderer.canvas_width/2 + 2),
self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 2 self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 2
@@ -142,8 +161,82 @@ class ScanScreen(BaseScreen):
font=instructions_font, font=instructions_font,
anchor="ms") anchor="ms")
else:
# Render the progress bar
rectangle = Image.new('RGBA', (self.renderer.canvas_width - 2*GUIConstants.EDGE_PADDING, GUIConstants.BUTTON_HEIGHT), (0, 0, 0, 0))
draw = ImageDraw.Draw(rectangle)
# Start with a background rounded rectangle, same dims as the buttons
overlay_color = (0, 0, 0, 191) # opacity ranges from 0-255
draw.rounded_rectangle(
(
(0, 0),
(rectangle.width, rectangle.height)
),
fill=overlay_color,
radius=8,
outline=overlay_color,
width=2,
)
progress_bar_thickness = 4
progress_bar_width = rectangle.width - 2*GUIConstants.EDGE_PADDING - progress_text_width - int(GUIConstants.EDGE_PADDING/2)
progress_bar_xy = (
(GUIConstants.EDGE_PADDING, int((rectangle.height - progress_bar_thickness) / 2)),
(GUIConstants.EDGE_PADDING + progress_bar_width, int(rectangle.height + progress_bar_thickness) / 2)
)
draw.rounded_rectangle(
progress_bar_xy,
fill=GUIConstants.INACTIVE_COLOR,
radius=8
)
progress_percentage = self.decoder.get_percent_complete(weight_mixed_frames=True)
draw.rounded_rectangle(
(
progress_bar_xy[0],
(GUIConstants.EDGE_PADDING + int(progress_percentage * progress_bar_width / 100.0), progress_bar_xy[1][1])
),
fill=GUIConstants.GREEN_INDICATOR_COLOR,
radius=8
)
draw.text(
xy=(rectangle.width - GUIConstants.EDGE_PADDING, int(rectangle.height / 2)),
text=f"{progress_percentage}%",
# text=f"100%",
fill=GUIConstants.BODY_FONT_COLOR,
font=instructions_font,
anchor="rm", # right-justified, middle
)
frame.paste(rectangle, (GUIConstants.EDGE_PADDING, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - rectangle.height), rectangle)
# Render the dot to indicate successful QR frame read
indicator_size = 10
self.last_frame_decoded_count = self.frames_decoded_counter.cur_count
status_color_map = {
ScanScreen.FRAME__ADDED_PART: GUIConstants.SUCCESS_COLOR,
ScanScreen.FRAME__REPEATED_PART: GUIConstants.INACTIVE_COLOR,
ScanScreen.FRAME__MISS: None,
}
status_color = status_color_map.get(self.frame_decode_status.cur_count)
if status_color:
# Good! Most recent frame successfully decoded.
# Draw the onscreen indicator dot
draw = ImageDraw.Draw(frame)
draw.ellipse(
(
(self.renderer.canvas_width - GUIConstants.EDGE_PADDING - indicator_size, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - GUIConstants.BUTTON_HEIGHT - GUIConstants.COMPONENT_PADDING - indicator_size),
(self.renderer.canvas_width - GUIConstants.EDGE_PADDING, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - GUIConstants.BUTTON_HEIGHT - GUIConstants.COMPONENT_PADDING)
),
fill=status_color,
outline="black",
width=1,
)
self.renderer.show_image(frame, show_direct=True) self.renderer.show_image(frame, show_direct=True)
# print(f" {cur_fps:0.2f} | {self.decoder_fps}")
if self.camera._video_stream is None: if self.camera._video_stream is None:
break break
@@ -169,6 +262,21 @@ class ScanScreen(BaseScreen):
if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID): if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID):
self.camera.stop_video_stream_mode() self.camera.stop_video_stream_mode()
break break
self.frames_decoded_counter.increment()
# Notify the live preview thread how our most recent decode went
if status == DecodeQRStatus.FALSE:
# Did not find anything to decode in the current frame
self.frames_decode_status.set_value(self.FRAME__MISS)
else:
if status == DecodeQRStatus.PART_COMPLETE:
# We received a valid frame that added new data
self.frames_decode_status.set_value(self.FRAME__ADDED_PART)
elif status == DecodeQRStatus.PART_EXISTING:
# We received a valid frame, but we've already seen in
self.frames_decode_status.set_value(self.FRAME__REPEATED_PART)
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT) or self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT): if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT) or self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT):
self.camera.stop_video_stream_mode() self.camera.stop_video_stream_mode()
+3 -2
View File
@@ -10,6 +10,7 @@ from seedsigner.gui.components import (GUIConstants,
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.gui.renderer import Renderer from seedsigner.gui.renderer import Renderer
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
from seedsigner.models.encode_qr import BaseQrEncoder
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import SettingsConstants
from seedsigner.models.threads import BaseThread, ThreadsafeCounter from seedsigner.models.threads import BaseThread, ThreadsafeCounter
@@ -657,10 +658,10 @@ class LargeButtonScreen(BaseTopNavScreen):
@dataclass @dataclass
class QRDisplayScreen(BaseScreen): class QRDisplayScreen(BaseScreen):
qr_encoder: 'EncodeQR' = None qr_encoder: BaseQrEncoder = None
class QRDisplayThread(BaseThread): class QRDisplayThread(BaseThread):
def __init__(self, qr_encoder: 'EncodeQR', qr_brightness: ThreadsafeCounter, renderer: Renderer, def __init__(self, qr_encoder: BaseQrEncoder, qr_brightness: ThreadsafeCounter, renderer: Renderer,
tips_start_time: ThreadsafeCounter): tips_start_time: ThreadsafeCounter):
super().__init__() super().__init__()
self.qr_encoder = qr_encoder self.qr_encoder = qr_encoder
+1 -1
View File
@@ -38,7 +38,7 @@ class Camera(Singleton):
return frame return frame
else: else:
if frame is not None: if frame is not None:
return Image.fromarray(frame.astype('uint8'), 'RGB').rotate(90 + self._camera_rotation) return Image.fromarray(frame.astype('uint8'), 'RGB').convert('RGBA').rotate(90 + self._camera_rotation)
return None return None
+71 -12
View File
@@ -2,21 +2,40 @@ import hashlib
import unicodedata import unicodedata
from embit import bip39 from embit import bip39
from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes from seedsigner.models.settings_definition import SettingsConstants
from typing import List from seedsigner.models.seed import Seed
"""
This is SeedSigner's internal mnemonic generation utility.
It can also be run as an independently-executable CLI to facilitate external
verification of SeedSigner's results for a given input entropy.
see: docs/dice_verification.md (the "Command Line Tool" section).
"""
DICE__NUM_ROLLS__12WORD = 50
DICE__NUM_ROLLS__24WORD = 99
def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]: def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
""" """
Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list. Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list.
Mnemonic may be a list of words or a string of words separated by spaces or commas.
If 11- or 23-words are provided, append word `0000` to end of list as temp final If 11- or 23-words are provided, append word `0000` to end of list as temp final
word. word.
""" """
from seedsigner.models.seed import Seed if type(mnemonic) == str:
import re
# split on commas or spaces
mnemonic = re.findall(r'[^,\s]+', mnemonic)
if len(mnemonic) in [11, 23]: if len(mnemonic) in [11, 23]:
mnemonic.append(Seed.get_wordlist(wordlist_language_code)[0]) temp_final_word = Seed.get_wordlist(wordlist_language_code)[0]
mnemonic.append(temp_final_word)
if len(mnemonic) not in [12, 24]: if len(mnemonic) not in [12, 24]:
raise Exception("Pass in a 12- or 24-word mnemonic") raise Exception("Pass in a 12- or 24-word mnemonic")
@@ -37,28 +56,68 @@ def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]
def generate_mnemonic_from_bytes(entropy_bytes) -> List[str]: def generate_mnemonic_from_bytes(entropy_bytes, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
return bip39.mnemonic_from_bytes(entropy_bytes).split() return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split()
def generate_mnemonic_from_dice(roll_data: str) -> List[str]: def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
"""
Takes a string of 50 or 99 dice rolls and returns a 12- or 24-word mnemonic.
Uses the iancoleman.io/bip39 and bitcoiner.guide/seed "Base 10" or "Hex" mode approach:
* dice rolls are treated as string data.
* hashed via SHA256.
Important note: This method is NOT compatible with iancoleman's "Dice" mode.
"""
entropy_bytes = hashlib.sha256(roll_data.encode()).digest() entropy_bytes = hashlib.sha256(roll_data.encode()).digest()
if len(roll_data) == 50: if len(roll_data) == DICE__NUM_ROLLS__12WORD:
# 12-word mnemonic; only use 128bits / 16 bytes # 12-word mnemonic; only use 128bits / 16 bytes
entropy_bytes = entropy_bytes[:16] entropy_bytes = entropy_bytes[:16]
# Return as a list # Return as a list
return bip39.mnemonic_from_bytes(entropy_bytes).split() return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split()
def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
"""
Takes a string of 128 or 256 0s and 1s and returns a 12- or 24-word mnemonic.
Uses the iancoleman.io/bip39 and bitcoiner.guide/seed "Binary" mode approach:
* binary digit stream is treated as string data.
* hashed via SHA256.
"""
entropy_bytes = hashlib.sha256(coin_flips.encode()).digest()
if len(coin_flips) == 128:
# 12-word mnemonic; only use 128bits / 16 bytes
entropy_bytes = entropy_bytes[:16]
# Return as a list
return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split()
def get_partial_final_word(coin_flips: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> str:
""" Look up the partial final word for the given coin flips.
7 coin flips: 0101010 + **** where the final 4 bits will be replaced with the checksum
3 coin flips: 010 + ******** where the final 8 bits will be replaced with the checksum
"""
binary_string = coin_flips + "0" * (11 - len(coin_flips))
wordlist_index = int(binary_string, 2)
return Seed.get_wordlist(wordlist_language_code)[wordlist_index]
# Note: This currently isn't being used since we're now chaining hashed bytes for the # Note: This currently isn't being used since we're now chaining hashed bytes for the
# image-based entropy and aren't just ingesting a single image. # image-based entropy and aren't just ingesting a single image.
def generate_mnemonic_from_image(image) -> List[str]: def generate_mnemonic_from_image(image, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
import hashlib import hashlib
hash = hashlib.sha256(image.tobytes()) hash = hashlib.sha256(image.tobytes())
# Return as a list # Return as a list
return bip39.mnemonic_from_bytes(hash.digest()).split() return bip39.mnemonic_from_bytes(hash.digest(), wordlist=Seed.get_wordlist(wordlist_language_code)).split()
+76 -13
View File
@@ -4,7 +4,7 @@
# Copyright © 2020 Foundation Devices, Inc. # Copyright © 2020 Foundation Devices, Inc.
# Licensed under the "BSD-2-Clause Plus Patent License" # Licensed under the "BSD-2-Clause Plus Patent License"
# #
import time
from .fountain_utils import choose_fragments, contains, is_strict_subset, set_difference from .fountain_utils import choose_fragments, contains, is_strict_subset, set_difference
from .utils import join_lists, join_bytes, crc32_int, xor_with, take_first from .utils import join_lists, join_bytes, crc32_int, xor_with, take_first
@@ -71,13 +71,53 @@ class FountainDecoder:
def result_error(self): def result_error(self):
return self.result return self.result
def estimated_percent_complete(self):
def estimated_percent_complete(self, weight_mixed_frames: bool = False):
"""
Weighted mixed frame method:
* counts completed frames
* counts each additional frame that is currently XORed in a mixed frame; its
score is weighted by the number of frames mixed together (1/num frames mixed).
"""
if self.is_complete(): if self.is_complete():
return 1 return 1
if self.expected_part_indexes == None: if self.expected_part_indexes == None:
return 0 return 0
estimated_input_parts = self.expected_part_count() * 1.75
return min(0.99, self.processed_parts_count / estimated_input_parts) if not weight_mixed_frames:
# Original estimation method
estimated_input_parts = self.expected_part_count() * 1.75
return min(0.99, self.processed_parts_count / estimated_input_parts)
else:
parts = self.expected_part_count() if self.expected_part_indexes != None else 'None'
mixed = []
mixed_index_scoring = {}
mixed_set = set()
for indexes, p in self.mixed_parts.items():
if not indexes:
continue
mixed.append(self.indexes_to_string(indexes))
mixed_set.update(indexes)
score = 1.0 / float(len(indexes))
for index in indexes:
if index not in mixed_index_scoring:
mixed_index_scoring[index] = 0.0
# sum up partial scores
mixed_index_scoring[index] += score
mixed_score = 0.0
for index, score in mixed_index_scoring.items():
# set a ceiling; don't let an index in a mixed/XOR frame
# achieve equal weight as a fully decoded frame. Also if
# the ceiling is too high, can potentially see your
# reported progress percentage DECREASE during a decode.
mixed_score += min(score, 0.75)
num_complete = len(self.received_part_indexes)
weighted_estimate = (num_complete + mixed_score) / float(parts)
return weighted_estimate
def receive_part(self, encoder_part): def receive_part(self, encoder_part):
# Don't process the part if we're already done # Don't process the part if we're already done
@@ -93,6 +133,9 @@ class FountainDecoder:
self.last_part_indexes = p.indexes self.last_part_indexes = p.indexes
self.enqueue(p) self.enqueue(p)
num_complete = len(self.received_part_indexes)
num_mixed_frames = len(self.mixed_parts)
# Process the queue until we're done or the queue is empty # Process the queue until we're done or the queue is empty
while not self.is_complete() and len(self.queued_parts) != 0: while not self.is_complete() and len(self.queued_parts) != 0:
self.process_queue_item() self.process_queue_item()
@@ -101,6 +144,12 @@ class FountainDecoder:
self.processed_parts_count += 1 self.processed_parts_count += 1
# self.print_part_end() # self.print_part_end()
# self.print_state()
if num_complete == len(self.received_part_indexes) and num_mixed_frames == len(self.mixed_parts):
# This part didn't add any new info
# print("No new data")
return False
return True return True
@@ -114,6 +163,7 @@ class FountainDecoder:
self.queued_parts.append(p) self.queued_parts.append(p)
def process_queue_item(self): def process_queue_item(self):
start = time.time()
part = self.queued_parts.pop(0) part = self.queued_parts.pop(0)
# self.print_part(part) # self.print_part(part)
@@ -121,6 +171,8 @@ class FountainDecoder:
self.process_simple_part(part) self.process_simple_part(part)
else: else:
self.process_mixed_part(part) self.process_mixed_part(part)
# print(f"Queue processing: {int((time.time() - start)*1000.0)}ms")
# self.print_state() # self.print_state()
def reduce_mixed_by(self, p): def reduce_mixed_by(self, p):
@@ -141,6 +193,7 @@ class FountainDecoder:
new_mixed[reduced_part.indexes] = reduced_part new_mixed[reduced_part.indexes] = reduced_part
self.mixed_parts = new_mixed self.mixed_parts = new_mixed
# print(self.mixed_parts.keys())
def reduce_part_by_part(self, a, b): def reduce_part_by_part(self, a, b):
# If the fragments mixed into `b` are a strict (proper) subset of those in `a`... # If the fragments mixed into `b` are a strict (proper) subset of those in `a`...
@@ -266,13 +319,23 @@ class FountainDecoder:
print("processed: {}, expected: {}, received: {}, percent: {}%".format(self.processed_parts_count, expected, len(self.received_part_indexes), percent)) print("processed: {}, expected: {}, received: {}, percent: {}%".format(self.processed_parts_count, expected, len(self.received_part_indexes), percent))
def print_state(self): def print_state(self):
parts = self.expected_part_count() if self.expected_part_indexes != None else 'None' guesstimate = self.estimated_percent_complete(weight_mixed_frames=True)
received = self.indexes_to_string(self.received_part_indexes) original_metric = self.estimated_percent_complete()
mixed = [] mixed = []
for indexes, p in self.mixed_parts.items(): mixed_set = set()
mixed.append(self.indexes_to_string(indexes)) try:
for indexes, p in self.mixed_parts.items():
mixed_s = "[{}]".format(', '.join(mixed)) if not indexes or len(indexes) == 0:
queued = len(self.queued_parts) continue
res = self.result_description() mixed.append(self.indexes_to_string(indexes))
print('parts: {}, received: {}, mixed: {}, queued: {}, result: {}'.format(parts, received, mixed_s, queued, res)) mixed_set.update(indexes)
num_complete = len(self.received_part_indexes)
mixed_s = "[{}]".format(', '.join(mixed))
queued = len(self.queued_parts)
print(f"{original_metric*100.0:5.1f}% | {guesstimate*100.0:5.1f}% | done: {num_complete:2d}, mixed: {len(mixed_set):2d}, queued: {queued}, frames: {self.processed_parts_count:2d} | {mixed_s}")
except Exception as e:
import traceback
traceback.print_exc()
+12 -1
View File
@@ -90,6 +90,7 @@ class FountainEncoder:
self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len) self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len)
self.fragments = FountainEncoder.partition_message(message, self.fragment_len) self.fragments = FountainEncoder.partition_message(message, self.fragment_len)
self.seq_num = first_seq_num self.seq_num = first_seq_num
self.current_part: Part = None
@staticmethod @staticmethod
def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len): def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len):
@@ -143,7 +144,17 @@ class FountainEncoder:
indexes = choose_fragments(self.seq_num, self.seq_len(), self.checksum) indexes = choose_fragments(self.seq_num, self.seq_len(), self.checksum)
mixed = self.mix(indexes) mixed = self.mix(indexes)
data = bytes(mixed) data = bytes(mixed)
return Part(self.seq_num, self.seq_len(), self.message_len, self.checksum, data) self.current_part = Part(self.seq_num, self.seq_len(), self.message_len, self.checksum, data)
return self.current_part
def restart(self):
"""
Restart from the beginning; each cycle's first n frames are full data frames
(not XOR composites).
"""
self.seq_num = 0
def mix(self, indexes): def mix(self, indexes):
result = [0] * self.fragment_len result = [0] * self.fragment_len
+2 -2
View File
@@ -153,8 +153,8 @@ class URDecoder:
def processed_parts_count(self): def processed_parts_count(self):
return self.fountain_decoder.processed_parts_count return self.fountain_decoder.processed_parts_count
def estimated_percent_complete(self): def estimated_percent_complete(self, weight_mixed_frames: bool = False):
return self.fountain_decoder.estimated_percent_complete() return self.fountain_decoder.estimated_percent_complete(weight_mixed_frames=weight_mixed_frames)
def is_success(self): def is_success(self):
result = self.result result = self.result
+16 -2
View File
@@ -34,12 +34,26 @@ class UREncoder:
def is_single_part(self): def is_single_part(self):
return self.fountain_encoder.is_single_part() return self.fountain_encoder.is_single_part()
def next_part(self): def next_part(self) -> str:
part = self.fountain_encoder.next_part()
if self.is_single_part(): if self.is_single_part():
return UREncoder.encode(self.ur) return UREncoder.encode(self.ur)
else: else:
part = self.fountain_encoder.next_part()
return UREncoder.encode_part(self.ur.type, part) return UREncoder.encode_part(self.ur.type, part)
def current_part(self) -> str:
if self.is_single_part():
return UREncoder.encode(self.ur)
else:
part = self.fountain_encoder.current_part
if not part:
part = self.fountain_encoder.next_part()
return UREncoder.encode_part(self.ur.type, part)
def restart(self):
self.fountain_encoder.restart()
@staticmethod @staticmethod
def encode_part(type, part): def encode_part(type, part):
+15 -11
View File
@@ -120,11 +120,14 @@ class DecodeQR:
qr_str = data qr_str = data
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]: if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
self.decoder.receive_part(qr_str) added_part = self.decoder.receive_part(qr_str)
if self.decoder.is_complete(): if self.decoder.is_complete():
self.complete = True self.complete = True
return DecodeQRStatus.COMPLETE return DecodeQRStatus.COMPLETE
return DecodeQRStatus.PART_COMPLETE # segment added to ur2 decoder if added_part:
return DecodeQRStatus.PART_COMPLETE
else:
return DecodeQRStatus.PART_EXISTING
else: else:
# All other formats use the same method signature # All other formats use the same method signature
@@ -219,12 +222,12 @@ class DecodeQR:
return self.decoder.get_wallet_descriptor() return self.decoder.get_wallet_descriptor()
def get_percent_complete(self) -> int: def get_percent_complete(self, weight_mixed_frames: bool = False) -> int:
if not self.decoder: if not self.decoder:
return 0 return 0
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]: if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
return int(self.decoder.estimated_percent_complete() * 100) return int(self.decoder.estimated_percent_complete(weight_mixed_frames=weight_mixed_frames) * 100)
elif self.qr_type in [QRType.PSBT__SPECTER]: elif self.qr_type in [QRType.PSBT__SPECTER]:
if self.decoder.total_segments == None: if self.decoder.total_segments == None:
@@ -261,6 +264,7 @@ class DecodeQR:
QRType.PSBT__BASE43, QRType.PSBT__BASE43,
] ]
@property @property
def is_seed(self): def is_seed(self):
return self.qr_type in [ return self.qr_type in [
@@ -305,7 +309,7 @@ class DecodeQR:
@staticmethod @staticmethod
def extract_qr_data(image, is_binary:bool = False) -> str: def extract_qr_data(image, is_binary:bool = False) -> str | None:
if image is None: if image is None:
return None return None
@@ -337,10 +341,10 @@ class DecodeQR:
# PSBT # PSBT
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE): if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
return QRType.PSBT__UR2 return QRType.PSBT__UR2
elif re.search("^UR:CRYPTO-OUTPUT/", s, re.IGNORECASE): elif re.search("^UR:CRYPTO-OUTPUT/", s, re.IGNORECASE):
return QRType.OUTPUT__UR return QRType.OUTPUT__UR
elif re.search("^UR:CRYPTO-ACCOUNT/", s, re.IGNORECASE): elif re.search("^UR:CRYPTO-ACCOUNT/", s, re.IGNORECASE):
return QRType.ACCOUNT__UR return QRType.ACCOUNT__UR
@@ -362,10 +366,10 @@ class DecodeQR:
elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', desc_str, re.IGNORECASE): elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', desc_str, re.IGNORECASE):
# if json starting with label and contains descriptor, assume specter wallet json # if json starting with label and contains descriptor, assume specter wallet json
return QRType.WALLET__SPECTER return QRType.WALLET__SPECTER
elif "multisig setup file" in s.lower(): elif "multisig setup file" in s.lower():
return QRType.WALLET__CONFIGFILE return QRType.WALLET__CONFIGFILE
elif "sortedmulti" in s: elif "sortedmulti" in s:
return QRType.WALLET__GENERIC return QRType.WALLET__GENERIC
@@ -392,7 +396,7 @@ class DecodeQR:
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist] _4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
except: except:
_4LETTER_WORDLIST = [] _4LETTER_WORDLIST = []
if all(x in wordlist for x in s.strip().split(" ")): if all(x in wordlist for x in s.strip().split(" ")):
# checks if all words in list are in bip39 word list # checks if all words in list are in bip39 word list
return QRType.SEED__MNEMONIC return QRType.SEED__MNEMONIC
@@ -408,7 +412,7 @@ class DecodeQR:
# Probably this isn't meant to be string data; check if it's valid byte data # Probably this isn't meant to be string data; check if it's valid byte data
# below. # below.
pass pass
# Is it byte data? # Is it byte data?
# 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR # 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR
if len(s) == 32 or len(s) == 16: if len(s) == 32 or len(s) == 16:
+217 -358
View File
@@ -2,7 +2,7 @@ import math
from embit import bip32 from embit import bip32
from embit.networks import NETWORKS from embit.networks import NETWORKS
from binascii import b2a_base64, hexlify from binascii import hexlify
from dataclasses import dataclass from dataclasses import dataclass
from typing import List from typing import List
from embit import bip32 from embit import bip32
@@ -11,8 +11,6 @@ from embit.psbt import PSBT
from seedsigner.helpers.ur2.ur_encoder import UREncoder from seedsigner.helpers.ur2.ur_encoder import UREncoder
from seedsigner.helpers.ur2.ur import UR from seedsigner.helpers.ur2.ur import UR
from seedsigner.helpers.qr import QR from seedsigner.helpers.qr import QR
from seedsigner.helpers import embit_utils
from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import Seed from seedsigner.models.seed import Seed
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import SettingsConstants
@@ -21,149 +19,62 @@ from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIP
@dataclass @dataclass
class EncodeQR: class BaseQrEncoder:
"""
Encode psbt for displaying as qr image
"""
# TODO: Refactor so that this is a base class with implementation classes for each
# QR type. No reason exterior code can't directly instantiate the encoder it needs.
# Dataclass input vars on __init__()
psbt: PSBT = None
seed: Seed = None
derivation: str = None
network: str = SettingsConstants.MAINNET
qr_type: str = None
qr_density: str = SettingsConstants.DENSITY__MEDIUM qr_density: str = SettingsConstants.DENSITY__MEDIUM
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
bitcoin_address: str = None
signed_message: str = None
sig_type : str = SettingsConstants.SINGLE_SIG
def __post_init__(self): def __post_init__(self):
self.qr = QR() self.qr = QR()
if not self.qr_type:
raise Exception('qr_type is required')
if self.qr_density == None:
self.qr_density = SettingsConstants.DENSITY__MEDIUM
self.encoder: BaseQrEncoder = None
# PSBT formats
if self.qr_type == QRType.PSBT__SPECTER:
self.encoder = SpecterPsbtQrEncoder(psbt=self.psbt, qr_density=self.qr_density)
elif self.qr_type == QRType.PSBT__UR2:
self.encoder = UrPsbtQrEncoder(psbt=self.psbt, qr_density=self.qr_density)
# XPUB formats
elif self.qr_type == QRType.XPUB:
self.encoder = XpubQrEncoder(
seed=self.seed,
derivation=self.derivation,
network=self.network,
wordlist_language_code=self.wordlist_language_code,
sig_type=self.sig_type
)
elif self.qr_type == QRType.XPUB__UR:
self.encoder = UrXpubQrEncoder(
qr_density=self.qr_density,
seed=self.seed,
derivation=self.derivation,
network=self.network,
wordlist_language_code=self.wordlist_language_code,
sig_type=self.sig_type
)
elif self.qr_type == QRType.XPUB__SPECTER:
self.encoder = SpecterXPubQrEncoder(
qr_density=self.qr_density,
seed=self.seed,
derivation=self.derivation,
network=self.network,
wordlist_language_code=self.wordlist_language_code,
sig_type=self.sig_type
)
# SeedQR formats
elif self.qr_type == QRType.SEED__SEEDQR:
self.encoder = SeedQrEncoder(seed_phrase=self.seed.mnemonic_list,
wordlist_language_code=self.wordlist_language_code)
elif self.qr_type == QRType.SEED__COMPACTSEEDQR:
self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed.mnemonic_list,
wordlist_language_code=self.wordlist_language_code)
# Misc formats
elif self.qr_type == QRType.BITCOIN_ADDRESS:
self.encoder = BitcoinAddressEncoder(address=self.bitcoin_address)
elif self.qr_type == QRType.SIGN_MESSAGE:
self.encoder = SignedMessageEncoder(signed_message=self.signed_message)
else:
raise Exception('QR Type not supported')
def total_parts(self) -> int:
return self.encoder.seq_len()
def next_part(self):
return self.encoder.next_part()
def part_to_image(self, part, width=240, height=240, border=3):
return self.qr.qrimage_io(part, width, height, border)
def next_part_image(self, width=240, height=240, border=3, background_color="bdbdbd"):
part = self.next_part()
if self.qr_type == QRType.SEED__SEEDQR:
return self.qr.qrimage(part, width, height, border)
else:
return self.qr.qrimage_io(part, width, height, border, background_color=background_color)
# TODO: Make these properties?
def is_complete(self):
return self.encoder.is_complete
def get_qr_density(self):
return self.qr_density
def get_qr_type(self):
return self.qr_type
class BaseQrEncoder:
def seq_len(self):
raise Exception("Not implemented in child class")
def next_part(self) -> str:
raise Exception("Not implemented in child class")
@property @property
def is_complete(self): def is_complete(self):
raise Exception("Not implemented in child class") raise Exception("Not implemented in child class")
@property
def qr_max_fragment_size(self):
raise Exception("Not implemented in child class")
def seq_len(self):
raise Exception("Not implemented in child class")
def next_part(self) -> str:
raise Exception("Not implemented in child class")
def cur_part(self) -> str:
raise Exception("Not implemented in child class")
def restart(self):
# only used by animated QR encoders
pass
def _create_parts(self): def _create_parts(self):
raise Exception("Not implemented in child class") raise Exception("Not implemented in child class")
def part_to_image(self, part, width, height, border: int = 3, background_color: str = "ffffff"):
return self.qr.qrimage_io(part, width, height, border, background_color=background_color)
def next_part_image(self, width=240, height=240, border=3, background_color="bdbdbd"):
part = self.next_part()
return self.part_to_image(part, width, height, border, background_color=background_color)
"""**************************************************************************************
STATIC QR encoders
**************************************************************************************"""
@dataclass
class BaseStaticQrEncoder(BaseQrEncoder): class BaseStaticQrEncoder(BaseQrEncoder):
def seq_len(self): def seq_len(self):
return 1 return 1
def cur_part(self) -> str:
""" static QRs only have a single part, which `next_part` always returns """
return self.next_part()
@property @property
@@ -172,86 +83,125 @@ class BaseStaticQrEncoder(BaseQrEncoder):
class BasePsbtQrEncoder(BaseQrEncoder): @dataclass
def __init__(self, psbt: PSBT): class SeedQrEncoder(BaseStaticQrEncoder):
self.psbt = psbt seed : Seed = None
def __post_init__(self):
self.wordlist = Seed.get_wordlist(self.seed.wordlist_language_code)
super().__post_init__()
self.data = ""
# Output as Numeric data format
for word in self.seed.mnemonic_list:
index = self.wordlist.index(word)
self.data += str("%04d" % index)
def next_part(self):
return self.data
class UrPsbtQrEncoder(BasePsbtQrEncoder): @dataclass
def __init__(self, psbt, qr_density): class CompactSeedQrEncoder(SeedQrEncoder):
super().__init__(psbt) def next_part(self):
self.qr_max_fragment_size = 20 # Output as binary data format
binary_str = ""
for word in self.seed.mnemonic_list:
index = self.wordlist.index(word)
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
binary_str += bin(index).split('b')[1].zfill(11)
# We can exclude the checksum bits at the end
if len(self.seed.mnemonic_list) == 24:
# 8 checksum bits in a 24-word seed
binary_str = binary_str[:-8]
elif len(self.seed.mnemonic_list) == 12:
# 4 checksum bits in a 12-word seed
binary_str = binary_str[:-4]
# Now convert to bytes, 8 bits at a time
as_bytes = bytearray()
for i in range(0, math.ceil(len(binary_str) / 8)):
# int conversion reads byte data as a string prefixed with '0b'
as_bytes.append(int('0b' + binary_str[i*8:(i+1)*8], 2))
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor()) # Must return data as `bytes` for `qrcode` to properly recognize it as byte data
return bytes(as_bytes)
if qr_density == SettingsConstants.DENSITY__LOW:
self.qr_max_fragment_size = 10
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
self.qr_max_fragment_size = 30
elif qr_density == SettingsConstants.DENSITY__HIGH:
self.qr_max_fragment_size = 120
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
def seq_len(self):
return self.ur2_encode.fountain_encoder.seq_len() @dataclass
class GenericStaticQrEncoder(BaseStaticQrEncoder):
data: str = None
def next_part(self):
return self.data
def next_part(self) -> str:
return self.ur2_encode.next_part().upper() @dataclass
class BaseXpubQrEncoder(BaseQrEncoder):
"""
Base Xpub QrEncoder for static and animated formats
"""
seed: Seed = None
derivation: str = None
network: str = SettingsConstants.MAINNET
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
def prep_xpub(self):
self.wordlist = Seed.get_wordlist()
if self.wordlist == None:
raise Exception('Wordlist Required')
version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)])
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"])
self.fingerprint = self.root.child(0).fingerprint
self.xprv = self.root.derive(self.derivation)
self.xpub = self.xprv.to_public()
self.xpub_base58 = self.xpub.to_string(version=version)
self.xpubstring = "[{}{}]{}".format(
hexlify(self.fingerprint).decode('utf-8'),
self.derivation[1:],
self.xpub_base58
)
class StaticXpubQrEncoder(BaseXpubQrEncoder, BaseStaticQrEncoder):
def __post_init__(self):
super().__post_init__()
self.prep_xpub()
def next_part(self):
self.prep_xpub()
return self.xpubstring
"""**************************************************************************************
Simple animated QR encoders
**************************************************************************************"""
@dataclass
class BaseSimpleAnimatedQREncoder(BaseQrEncoder):
def __post_init__(self):
super().__post_init__()
self.parts = []
self.part_num_sent = 0
self.sent_complete = False
self._create_parts()
@property @property
def is_complete(self): def is_complete(self):
return self.ur2_encode.is_complete() return self.sent_complete
class SpecterPsbtQrEncoder(BasePsbtQrEncoder):
def __init__(self, psbt, qr_density):
super().__init__(psbt)
self.qr_max_fragement_size = 65
self.parts = []
self.part_num_sent = 0
self.sent_complete = False
if qr_density == SettingsConstants.DENSITY__LOW:
self.qr_max_fragement_size = 40
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
self.qr_max_fragement_size = 65
elif qr_density == SettingsConstants.DENSITY__HIGH:
self.qr_max_fragement_size = 90
self._create_parts()
def _create_parts(self):
base64_psbt = b2a_base64(self.psbt.serialize())
if base64_psbt[-1:] == b"\n":
base64_psbt = base64_psbt[:-1]
base64_psbt = base64_psbt.decode('utf-8')
start = 0
stop = self.qr_max_fragement_size
qr_cnt = ((len(base64_psbt)-1) // self.qr_max_fragement_size) + 1
if qr_cnt == 1:
self.parts.append(base64_psbt[start:stop])
cnt = 0
while cnt < qr_cnt and qr_cnt != 1:
part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + base64_psbt[start:stop]
self.parts.append(part)
start = start + self.qr_max_fragement_size
stop = stop + self.qr_max_fragement_size
if stop > len(base64_psbt):
stop = len(base64_psbt)
cnt += 1
def seq_len(self): def seq_len(self):
@@ -275,154 +225,34 @@ class SpecterPsbtQrEncoder(BasePsbtQrEncoder):
return part return part
@property def cur_part(self) -> str:
def is_complete(self): if self.part_num_sent == 0:
return self.sent_complete # Rewind all the way back to the end
self.part_num_sent = len(self.parts) - 1
else:
self.part_num_sent -= 1
return self.next_part()
def restart(self) -> str:
class SeedQrEncoder(BaseStaticQrEncoder):
def __init__(self, seed_phrase: List[str], wordlist_language_code: str):
super().__init__()
self.seed_phrase = seed_phrase
self.wordlist = Seed.get_wordlist(wordlist_language_code)
if self.wordlist == None:
raise Exception('Wordlist Required')
def next_part(self):
data = ""
# Output as Numeric data format
for word in self.seed_phrase:
index = self.wordlist.index(word)
data += str("%04d" % index)
return data
class CompactSeedQrEncoder(SeedQrEncoder):
def next_part(self):
# Output as binary data format
binary_str = ""
for word in self.seed_phrase:
index = self.wordlist.index(word)
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
binary_str += bin(index).split('b')[1].zfill(11)
# We can exclude the checksum bits at the end
if len(self.seed_phrase) == 24:
# 8 checksum bits in a 24-word seed
binary_str = binary_str[:-8]
elif len(self.seed_phrase) == 12:
# 4 checksum bits in a 12-word seed
binary_str = binary_str[:-4]
# Now convert to bytes, 8 bits at a time
as_bytes = bytearray()
for i in range(0, math.ceil(len(binary_str) / 8)):
# int conversion reads byte data as a string prefixed with '0b'
as_bytes.append(int('0b' + binary_str[i*8:(i+1)*8], 2))
# Must return data as `bytes` for `qrcode` to properly recognize it as byte data
return bytes(as_bytes)
class BitcoinAddressEncoder(BaseStaticQrEncoder):
def __init__(self, address: str):
super().__init__()
self.address = address
def next_part(self):
return self.address
class SignedMessageEncoder(BaseStaticQrEncoder):
"""
Assumes that a signed message will fit in a single-frame QR
"""
def __init__(self, signed_message: str):
super().__init__()
self.signed_message = signed_message
def next_part(self):
return self.signed_message
class XpubQrEncoder(BaseQrEncoder):
def __init__(self, seed, derivation, network, wordlist_language_code, sig_type : str = SettingsConstants.SINGLE_SIG):
self.seed = seed
self.derivation = derivation
self.network = network
self.wordlist = Seed.get_wordlist(wordlist_language_code)
self.parts = []
self.part_num_sent = 0 self.part_num_sent = 0
self.sent_complete = False
if self.wordlist == None:
raise Exception('Wordlist Required')
version = self.seed.detect_version(self.derivation, network, sig_type)
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"])
self.fingerprint = self.root.child(0).fingerprint
self.xprv = self.root.derive(self.derivation)
self.xpub = self.xprv.to_public()
self.xpub_base58 = self.xpub.to_string(version=version)
self.xpubstring = "[{}{}]{}".format(
hexlify(self.fingerprint).decode('utf-8'),
self.derivation[1:],
self.xpub_base58
)
self._create_parts()
def _create_parts(self):
self.parts = []
self.parts.append(self.xpubstring)
def next_part(self) -> str:
if len(self.parts) > 0:
self.sent_complete = True
return self.parts[0]
def seq_len(self):
return len(self.parts)
@dataclass
class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder):
@property @property
def is_complete(self): def qr_max_fragment_size(self):
return self.sent_complete density_mapping = {
SettingsConstants.DENSITY__LOW: 40,
SettingsConstants.DENSITY__MEDIUM: 65,
SettingsConstants.DENSITY__HIGH: 90,
class SpecterXPubQrEncoder(XpubQrEncoder): }
def __init__(self, qr_density, **kwargs): return density_mapping.get(self.qr_density, 65)
# Must set up qr_max_fragment_size before calling super().__init__()
self.qr_max_fragment_size = 65
if qr_density == SettingsConstants.DENSITY__LOW:
self.qr_max_fragment_size = 40
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
self.qr_max_fragment_size = 65
elif qr_density == SettingsConstants.DENSITY__HIGH:
self.qr_max_fragment_size = 90
super().__init__(**kwargs)
def _create_parts(self): def _create_parts(self):
self.parts = [] self.prep_xpub()
start = 0 start = 0
stop = self.qr_max_fragment_size stop = self.qr_max_fragment_size
qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1 qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1
@@ -442,34 +272,60 @@ class SpecterXPubQrEncoder(XpubQrEncoder):
cnt += 1 cnt += 1
"""**************************************************************************************
Fountain encoded animated QR encoders
**************************************************************************************"""
@dataclass
class BaseFountainQrEncoder(BaseQrEncoder):
def __post_init__(self):
super().__post_init__()
self.ur2_encode: UREncoder = None
@property
def is_complete(self):
return self.ur2_encode.is_complete()
@property
def qr_max_fragment_size(self):
density_mapping = {
SettingsConstants.DENSITY__LOW: 10,
SettingsConstants.DENSITY__MEDIUM: 30,
SettingsConstants.DENSITY__HIGH: 120,
}
return density_mapping.get(self.qr_density, 30)
def _create_parts(self):
""" parts are dynamically generated by the fountain encoder """
pass
def seq_len(self):
return self.ur2_encode.fountain_encoder.seq_len()
def next_part(self) -> str: def next_part(self) -> str:
# if part num sent is gt number of parts, start at 0 return self.ur2_encode.next_part().upper()
if self.part_num_sent > (len(self.parts) - 1):
self.part_num_sent = 0
part = self.parts[self.part_num_sent]
# when parts sent eq num of parts in list def cur_part(self) -> str:
if self.part_num_sent == (len(self.parts) - 1): return self.ur2_encode.current_part().upper()
self.sent_complete = True
# increment to next part def restart(self):
self.part_num_sent += 1 self.ur2_encode.fountain_encoder.restart()
return part
class UrXpubQrEncoder(XpubQrEncoder): @dataclass
def __init__(self, qr_density, **kwargs): class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder):
super().__init__(**kwargs) def __post_init__(self):
super().__post_init__()
if qr_density == SettingsConstants.DENSITY__LOW: self.prep_xpub()
self.qr_max_fragment_size = 10
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
self.qr_max_fragment_size = 30
elif qr_density == SettingsConstants.DENSITY__HIGH:
self.qr_max_fragment_size = 120
def derivation_to_keypath(path: str) -> list: def derivation_to_keypath(path: str) -> list:
arr = path.split("/") arr = path.split("/")
@@ -527,9 +383,12 @@ class UrXpubQrEncoder(XpubQrEncoder):
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size) self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
def seq_len(self):
return self.ur2_encode.fountain_encoder.seq_len()
@dataclass
class UrPsbtQrEncoder(BaseFountainQrEncoder):
psbt: PSBT = None
def next_part(self) -> str: def __post_init__(self):
return self.ur2_encode.next_part().upper() super().__post_init__()
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor())
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
+6 -3
View File
@@ -20,7 +20,7 @@ class Seed:
mnemonic: List[str] = None, mnemonic: List[str] = None,
passphrase: str = "", passphrase: str = "",
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> None: wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> None:
self.wordlist_language_code = wordlist_language_code self._wordlist_language_code = wordlist_language_code
if not mnemonic: if not mnemonic:
raise Exception("Must initialize a Seed with a mnemonic List[str]") raise Exception("Must initialize a Seed with a mnemonic List[str]")
@@ -58,7 +58,10 @@ class Seed:
@property @property
def mnemonic_list(self) -> List[str]: def mnemonic_list(self) -> List[str]:
return self._mnemonic return self._mnemonic
@property
def wordlist_language_code(self) -> str:
return self._wordlist_language_code
@property @property
def mnemonic_display_str(self) -> str: def mnemonic_display_str(self) -> str:
@@ -95,7 +98,7 @@ class Seed:
@property @property
def wordlist(self) -> List[str]: def wordlist(self) -> List[str]:
return Seed.get_wordlist(self.wordlist_language_code) return Seed.get_wordlist(self._wordlist_language_code)
def set_wordlist_language_code(self, language_code: str): def set_wordlist_language_code(self, language_code: str):
+1 -1
View File
@@ -430,7 +430,7 @@ class SettingsDefinition:
type=SettingsConstants.TYPE__MULTISELECT, type=SettingsConstants.TYPE__MULTISELECT,
visibility=SettingsConstants.VISIBILITY__ADVANCED, visibility=SettingsConstants.VISIBILITY__ADVANCED,
selection_options=SettingsConstants.ALL_SCRIPT_TYPES, selection_options=SettingsConstants.ALL_SCRIPT_TYPES,
default_value=[SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]), default_value=[SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TAPROOT]),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__XPUB_DETAILS, attr_name=SettingsConstants.SETTING__XPUB_DETAILS,
+6 -7
View File
@@ -4,9 +4,8 @@ from embit.networks import NETWORKS
from seedsigner.controller import Controller from seedsigner.controller import Controller
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.encode_qr import UrPsbtQrEncoder
from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.psbt_parser import PSBTParser
from seedsigner.models.qr_type import QRType
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import SettingsConstants
from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, QRDisplayScreen) from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, QRDisplayScreen)
@@ -300,6 +299,7 @@ class PSBTAddressDetailsView(View):
class PSBTChangeDetailsView(View): class PSBTChangeDetailsView(View):
NEXT = "Next" NEXT = "Next"
SKIP_VERIFICATION = "Skip Verificiation"
VERIFY_MULTISIG = "Verify Multisig Change" VERIFY_MULTISIG = "Verify Multisig Change"
@@ -360,7 +360,7 @@ class PSBTChangeDetailsView(View):
else: else:
# Have the Screen offer to load in the multisig descriptor. # Have the Screen offer to load in the multisig descriptor.
button_data = [self.VERIFY_MULTISIG, self.NEXT] button_data = [self.VERIFY_MULTISIG, self.SKIP_VERIFICATION]
else: else:
# Single sig # Single sig
@@ -428,7 +428,8 @@ class PSBTChangeDetailsView(View):
if selected_menu_num == RET_CODE__BACK_BUTTON: if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView) return Destination(BackStackView)
elif button_data[selected_menu_num] == self.NEXT: elif button_data[selected_menu_num] == self.NEXT or button_data[selected_menu_num] == self.SKIP_VERIFICATION:
if self.change_address_num < psbt_parser.num_change_outputs - 1: if self.change_address_num < psbt_parser.num_change_outputs - 1:
return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1}) return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1})
else: else:
@@ -513,11 +514,9 @@ class PSBTFinalizeView(View):
class PSBTSignedQRDisplayView(View): class PSBTSignedQRDisplayView(View):
def run(self): def run(self):
qr_encoder = EncodeQR( qr_encoder = UrPsbtQrEncoder(
psbt=self.controller.psbt, psbt=self.controller.psbt,
qr_type=QRType.PSBT__UR2, # All coordinators (as of 2022-08) use this format
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY), qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
) )
self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder) self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder)
+4
View File
@@ -47,6 +47,10 @@ class ScanView(View):
decoder=self.decoder decoder=self.decoder
) )
# A long scan might have exceeded the screensaver timeout; ensure screensaver
# doesn't immediately engage when we leave here.
self.controller.reset_screensaver_timeout()
# Handle the results # Handle the results
if self.decoder.is_complete: if self.decoder.is_complete:
if not self.is_valid_qr_type: if not self.is_valid_qr_type:
+38 -42
View File
@@ -16,7 +16,7 @@ from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen
from seedsigner.helpers import embit_utils from seedsigner.helpers import embit_utils
from seedsigner.models.decode_qr import DecodeQR from seedsigner.models.decode_qr import DecodeQR
from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.encode_qr import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder
from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.psbt_parser import PSBTParser
from seedsigner.models.qr_type import QRType from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import InvalidSeedException, Seed from seedsigner.models.seed import InvalidSeedException, Seed
@@ -754,20 +754,25 @@ class SeedExportXpubCoordinatorView(View):
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0] args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]
return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True) return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True)
button_data = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)
selected_menu_num = self.run_screen( selected_menu_num = self.run_screen(
ButtonListScreen, ButtonListScreen,
title="Export Xpub", title="Export Xpub",
is_button_text_centered=False, is_button_text_centered=False,
button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS), button_data=button_data,
) )
if selected_menu_num < len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)): if selected_menu_num == RET_CODE__BACK_BUTTON:
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[selected_menu_num]
return Destination(SeedExportXpubWarningView, view_args=args)
elif selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView) return Destination(BackStackView)
coordinators_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS)
selected_display_name = button_data[selected_menu_num]
args["coordinator"] = coordinators_settings_entry.get_selection_option_value_by_display_name(selected_display_name)
return Destination(SeedExportXpubWarningView, view_args=args)
class SeedExportXpubWarningView(View): class SeedExportXpubWarningView(View):
@@ -898,35 +903,24 @@ class SeedExportXpubQRDisplayView(View):
super().__init__() super().__init__()
self.seed = self.controller.get_seed(seed_num) self.seed = self.controller.get_seed(seed_num)
qr_density = self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) encoder_args = dict(
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
qr_type = QRType.XPUB__SPECTER
elif coordinator == SettingsConstants.COORDINATOR__BLUE_WALLET:
qr_type = QRType.XPUB
elif coordinator == SettingsConstants.COORDINATOR__KEEPER:
qr_type = QRType.XPUB
elif coordinator == SettingsConstants.COORDINATOR__NUNCHUK:
qr_type = QRType.XPUB__UR
# As of 2022-03-02 Nunchuk doesn't seem to support animated QRs for Xpub import
qr_density = SettingsConstants.DENSITY__HIGH
else:
qr_type = QRType.XPUB__UR
self.qr_encoder = EncodeQR(
seed=self.seed, seed=self.seed,
derivation=derivation_path, derivation=derivation_path,
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
qr_type=qr_type, qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY)
qr_density=qr_density,
wordlist_language_code=self.seed.wordlist_language_code,
sig_type=sig_type
) )
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
self.qr_encoder = SpecterXPubQrEncoder(**encoder_args)
elif coordinator in [SettingsConstants.COORDINATOR__BLUE_WALLET,
SettingsConstants.COORDINATOR__KEEPER,
SettingsConstants.COORDINATOR__NUNCHUK]:
self.qr_encoder = StaticXpubQrEncoder(**encoder_args)
else:
self.qr_encoder = UrXpubQrEncoder(**encoder_args)
def run(self): def run(self):
self.run_screen( self.run_screen(
@@ -1426,11 +1420,12 @@ class SeedTranscribeSeedQRWholeQRView(View):
def run(self): def run(self):
e = EncodeQR( encoder_args = dict(seed=self.seed)
seed=self.seed, if self.seedqr_format == QRType.SEED__SEEDQR:
qr_type=self.seedqr_format, e = SeedQrEncoder(**encoder_args)
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
) e = CompactSeedQrEncoder(**encoder_args)
data = e.next_part() data = e.next_part()
ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen( ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen(
@@ -1461,11 +1456,12 @@ class SeedTranscribeSeedQRZoomedInView(View):
def run(self): def run(self):
e = EncodeQR( encoder_args = dict(seed=self.seed)
seed=self.seed, if self.seedqr_format == QRType.SEED__SEEDQR:
qr_type=self.seedqr_format, e = SeedQrEncoder(**encoder_args)
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
) e = CompactSeedQrEncoder(**encoder_args)
data = e.next_part() data = e.next_part()
if len(self.seed.mnemonic_list) == 24: if len(self.seed.mnemonic_list) == 24:
@@ -2108,7 +2104,7 @@ class SeedSignMessageSignedMessageQRView(View):
def run(self): def run(self):
qr_encoder = EncodeQR(qr_type=QRType.SIGN_MESSAGE, signed_message=self.signed_message) qr_encoder = GenericStaticQrEncoder(data=self.signed_message)
self.run_screen( self.run_screen(
QRDisplayScreen, QRDisplayScreen,
+6 -7
View File
@@ -14,8 +14,7 @@ from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen,
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen) ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen)
from seedsigner.helpers import embit_utils, mnemonic_generation from seedsigner.helpers import embit_utils, mnemonic_generation
from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.encode_qr import GenericStaticQrEncoder
from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import Seed from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants from seedsigner.models.settings_definition import SettingsConstants
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView, SeedExportXpubScriptTypeView from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView, SeedExportXpubScriptTypeView
@@ -188,8 +187,8 @@ class ToolsImageEntropyMnemonicLengthView(View):
****************************************************************************""" ****************************************************************************"""
class ToolsDiceEntropyMnemonicLengthView(View): class ToolsDiceEntropyMnemonicLengthView(View):
def run(self): def run(self):
TWELVE = "12 words (50 rolls)" TWELVE = f"12 words ({mnemonic_generation.DICE__NUM_ROLLS__12WORD} rolls)"
TWENTY_FOUR = "24 words (99 rolls)" TWENTY_FOUR = f"24 words ({mnemonic_generation.DICE__NUM_ROLLS__24WORD} rolls)"
button_data = [TWELVE, TWENTY_FOUR] button_data = [TWELVE, TWENTY_FOUR]
selected_menu_num = ButtonListScreen( selected_menu_num = ButtonListScreen(
@@ -203,10 +202,10 @@ class ToolsDiceEntropyMnemonicLengthView(View):
return Destination(BackStackView) return Destination(BackStackView)
elif button_data[selected_menu_num] == TWELVE: elif button_data[selected_menu_num] == TWELVE:
return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=50)) return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=mnemonic_generation.DICE__NUM_ROLLS__12WORD))
elif button_data[selected_menu_num] == TWENTY_FOUR: elif button_data[selected_menu_num] == TWENTY_FOUR:
return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=99)) return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=mnemonic_generation.DICE__NUM_ROLLS__24WORD))
@@ -713,7 +712,7 @@ class ToolsAddressExplorerAddressView(View):
def run(self): def run(self):
from seedsigner.gui.screens.screen import QRDisplayScreen from seedsigner.gui.screens.screen import QRDisplayScreen
qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address) qr_encoder = GenericStaticQrEncoder(data=self.address)
self.run_screen( self.run_screen(
QRDisplayScreen, QRDisplayScreen,
qr_encoder=qr_encoder, qr_encoder=qr_encoder,
+1 -1
View File
@@ -1,6 +1,6 @@
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
from mock import MagicMock, Mock, patch from unittest.mock import MagicMock, Mock, patch
from typing import Callable from typing import Callable
# Prevent importing modules w/Raspi hardware dependencies. # Prevent importing modules w/Raspi hardware dependencies.
+2 -3
View File
@@ -1,4 +1,3 @@
coverage==7.2.1 coverage==7.3.1
mock==4.0.3 pytest==7.4.2
pytest==6.2.4
pytest-cov==4.1.0 pytest-cov==4.1.0
+1 -1
View File
@@ -2,7 +2,7 @@ import embit
import os import os
import sys import sys
import time import time
from mock import Mock, patch, MagicMock from unittest.mock import Mock, patch, MagicMock
from seedsigner.helpers import embit_utils from seedsigner.helpers import embit_utils
from seedsigner.models.settings import Settings from seedsigner.models.settings import Settings
+1 -1
View File
@@ -1,5 +1,5 @@
import pytest import pytest
from mock import MagicMock from unittest.mock import MagicMock
from seedsigner.models.seed import Seed from seedsigner.models.seed import Seed
from embit import bip39 from embit import bip39
+1 -1
View File
@@ -99,7 +99,7 @@ class TestController(BaseTest):
assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES] assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES]
assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT] assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TAPROOT]
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__180 assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__180
File diff suppressed because one or more lines are too long
+2 -3
View File
@@ -1,6 +1,5 @@
from base import FlowTest, FlowStep from base import FlowTest, FlowStep
from seedsigner.controller import Controller
from seedsigner.views.view import MainMenuView from seedsigner.views.view import MainMenuView
from seedsigner.views import scan_views, seed_views, psbt_views from seedsigner.views import scan_views, seed_views, psbt_views
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import SettingsConstants
@@ -74,7 +73,7 @@ class TestPSBTFlows(FlowTest):
FlowStep(psbt_views.PSBTOverviewView), FlowStep(psbt_views.PSBTOverviewView),
FlowStep(psbt_views.PSBTMathView), FlowStep(psbt_views.PSBTMathView),
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0), FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0),
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT), FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.SKIP_VERIFICATION),
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT), FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED), FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED),
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED), FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
@@ -86,7 +85,7 @@ class TestPSBTFlows(FlowTest):
FlowStep(psbt_views.PSBTOverviewView), FlowStep(psbt_views.PSBTOverviewView),
FlowStep(psbt_views.PSBTMathView), FlowStep(psbt_views.PSBTMathView),
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0), FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0),
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT), FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.SKIP_VERIFICATION),
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT), FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
FlowStep(psbt_views.PSBTSignedQRDisplayView), FlowStep(psbt_views.PSBTSignedQRDisplayView),
FlowStep(MainMenuView), FlowStep(MainMenuView),
+1 -1
View File
@@ -1,7 +1,7 @@
import os import os
from typing import Callable from typing import Callable
from mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
# Must import test base before the Controller # Must import test base before the Controller
from base import FlowTest, FlowStep from base import FlowTest, FlowStep
+1 -1
View File
@@ -1,4 +1,4 @@
from mock import patch from unittest.mock import patch
# Must import test base before the Controller # Must import test base before the Controller
from base import FlowTest, FlowStep from base import FlowTest, FlowStep
+40 -17
View File
@@ -12,16 +12,17 @@ def test_dice_rolls():
dice_rolls = "" dice_rolls = ""
for i in range(0, 99): for i in range(0, 99):
# Do not need truly rigorous random for this test # Do not need truly rigorous random for this test
dice_rolls += str(random.randint(0, 5)) dice_rolls += str(random.randint(1, 6))
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls) mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
assert len(mnemonic) == 24 assert len(mnemonic) == 24
assert bip39.mnemonic_is_valid(" ".join(mnemonic)) assert bip39.mnemonic_is_valid(" ".join(mnemonic))
dice_rolls = "" dice_rolls = ""
for i in range(0, 50): for i in range(0, mnemonic_generation.DICE__NUM_ROLLS__12WORD):
# Do not need truly rigorous random for this test # Do not need truly rigorous random for this test
dice_rolls += str(random.randint(0, 5)) dice_rolls += str(random.randint(1, 6))
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls) mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
assert len(mnemonic) == 12 assert len(mnemonic) == 12
@@ -29,18 +30,40 @@ def test_dice_rolls():
def test_calculate_checksum(): def test_calculate_checksum_input_type():
""" Given an 11-word or 23-word mnemonic, the calculated checksum should yield a """
Given an 11-word or 23-word mnemonic, the calculated checksum should yield a
valid complete mnemonic. valid complete mnemonic.
calculate_checksum should accept the mnemonic as:
* a list of strings
* string: "A B C", "A, B, C", "A,B,C"
""" """
# Test mnemonics from https://iancoleman.io/bip39/ # Test mnemonics from https://iancoleman.io/bip39/
def _try_all_input_formats(partial_mnemonic: str):
# List of strings
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "))
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
# Comma-separated string
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.replace(" ", ","))
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
# Comma-separated string w/space
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.replace(" ", ", "))
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
# Space-separated string
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic)
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day" partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day"
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) _try_all_input_formats(partial_mnemonic)
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile" partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile"
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) _try_all_input_formats(partial_mnemonic)
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
def test_calculate_checksum_invalid_mnemonics(): def test_calculate_checksum_invalid_mnemonics():
@@ -50,25 +73,25 @@ def test_calculate_checksum_invalid_mnemonics():
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:
# Mnemonic is too short: 10 words instead of 11 # Mnemonic is too short: 10 words instead of 11
partial_mnemonic = "abandon " * 9 + "about" partial_mnemonic = "abandon " * 9 + "about"
mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) mnemonic_generation.calculate_checksum(partial_mnemonic)
assert "12- or 24-word" in str(e) assert "12- or 24-word" in str(e)
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:
# Valid mnemonic but unsupported length # Valid mnemonic but unsupported length
mnemonic = "devote myth base logic dust horse nut collect buddy element eyebrow visit empty dress jungle" mnemonic = "devote myth base logic dust horse nut collect buddy element eyebrow visit empty dress jungle"
mnemonic_generation.calculate_checksum(mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) mnemonic_generation.calculate_checksum(mnemonic)
assert "12- or 24-word" in str(e) assert "12- or 24-word" in str(e)
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:
# Mnemonic is too short: 22 words instead of 23 # Mnemonic is too short: 22 words instead of 23
partial_mnemonic = "abandon " * 21 + "about" partial_mnemonic = "abandon " * 21 + "about"
mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) mnemonic_generation.calculate_checksum(partial_mnemonic)
assert "12- or 24-word" in str(e) assert "12- or 24-word" in str(e)
with pytest.raises(ValueError) as e: with pytest.raises(ValueError) as e:
# Invalid BIP-39 word # Invalid BIP-39 word
partial_mnemonic = "foobar " * 11 + "about" partial_mnemonic = "foobar " * 11 + "about"
mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) mnemonic_generation.calculate_checksum(partial_mnemonic)
assert "not in the dictionary" in str(e) assert "not in the dictionary" in str(e)
@@ -78,17 +101,17 @@ def test_calculate_checksum_with_default_final_word():
the mnemonic. the mnemonic.
""" """
partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day" partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day"
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic)
partial_mnemonic += " abandon" partial_mnemonic += " abandon"
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic)
assert mnemonic1 == mnemonic2 assert mnemonic1 == mnemonic2
partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile" partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile"
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic)
partial_mnemonic += " abandon" partial_mnemonic += " abandon"
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic)
assert mnemonic1 == mnemonic2 assert mnemonic1 == mnemonic2
+9 -10
View File
@@ -1,10 +1,8 @@
import os import os
import pyzbar
from embit import bip39 from embit import bip39
from seedsigner.helpers.qr import QR from seedsigner.helpers.qr import QR
from seedsigner.helpers.ur2.bytewords import decode
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder
from seedsigner.models.qr_type import QRType from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import Seed from seedsigner.models.seed import Seed
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import SettingsConstants
@@ -13,12 +11,14 @@ from seedsigner.models.settings import SettingsConstants
def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
""" Helper method to re-run multiple variations of the same encode/decode test """ """ Helper method to re-run multiple variations of the same encode/decode test """
print(entropy) mnemonic = bip39.mnemonic_from_bytes(entropy).split()
seed_phrase = bip39.mnemonic_from_bytes(entropy).split() assert len(mnemonic) == mnemonic_length
print(seed_phrase)
assert len(seed_phrase) == mnemonic_length if qr_type == QRType.SEED__SEEDQR:
e = SeedQrEncoder(seed=Seed(mnemonic))
elif qr_type == QRType.SEED__COMPACTSEEDQR:
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
e = EncodeQR(seed=Seed(seed_phrase), qr_type=qr_type)
data = e.next_part() data = e.next_part()
print(data) print(data)
@@ -35,8 +35,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
assert status == DecodeQRStatus.COMPLETE assert status == DecodeQRStatus.COMPLETE
decoded_seed_phrase = decoder.get_seed_phrase() decoded_seed_phrase = decoder.get_seed_phrase()
print(decoded_seed_phrase) assert mnemonic == decoded_seed_phrase
assert seed_phrase == decoded_seed_phrase
+169
View File
@@ -0,0 +1,169 @@
import argparse
import random
from seedsigner.helpers import mnemonic_generation
from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH
"""
see: docs/dice_verification.md (the "Command Line Tool" section) for full instructions.
tldr:
pip3 install embit
pip3 install -e .
cd tools
python3 mnemonic.py -h
"""
usage = f"""
Verify SeedSigner's dice rolls and coin flip entropy-to-mnemonic conversion via this tool.
Compare its results against iancoleman.io/bip39 and bitcoiner.guide/seed
Usage:
# {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls / 12-word mnemonic
python3 mnemonic.py dice 5624433434...
# {mnemonic_generation.DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic
python3 mnemonic.py dice 6151463561...
# {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic
python3 mnemonic.py --zero-indexed-dice dice 5135535514...
# 128 coin flips / 12-word mnemonic
python3 mnemonic.py coins 1111100111...
# 256 coin flips / 24-word mnemonic
python mnemonic.py coins 0010111010...
# GENERATE {mnemonic_generation.DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic
python3 mnemonic.py dice rand12
# GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic
python3 mnemonic.py dice rand24
# GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic
python3 mnemonic.py --zero-indexed-dice dice rand24
# GENERATE 128 random coin flips / 12-word mnemonic
python3 mnemonic.py coins rand12
# GENERATE 256 random coin flips / 24-word mnemonic
python3 mnemonic.py coins rand24
"""
RAND_12 = "rand12"
RAND_24 = "rand24"
parser = argparse.ArgumentParser(description=f'SeedSigner entropy-to-mnemonic tool\n\n{usage}', formatter_class=argparse.RawTextHelpFormatter)
# Required positional arguments
parser.add_argument('method', type=str, choices=['dice', 'coins', 'final_word'], help="Input entropy method")
parser.add_argument('entropy', type=str, help=f"""Entropy data. Enter "{ RAND_12 }" or "{ RAND_24 }" to create a random (not-secure) example seed.""")
# Optional arguments
parser.add_argument('-z', '--zero-indexed-dice',
action="store_true",
default=False,
dest="zero_indexed_dice",
help="Enables dice entry as [0-5] instead of default [1-6]")
args = parser.parse_args()
method = args.method
entropy = args.entropy
zero_indexed_dice = args.zero_indexed_dice
is_rand_seed = 'rand' in entropy
if is_rand_seed:
# Generate random data as our entropy
if entropy not in [RAND_12, RAND_24]:
print(f"""Invalid random entropy value: Must be either "{RAND_12}" or "{RAND_24}".""")
exit(1)
mnemonic_length = 12 if entropy == RAND_12 else 24
if method == 'dice':
if zero_indexed_dice:
entropy = ''.join([str(random.randint(0, 5)) for i in range(mnemonic_generation.DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else mnemonic_generation.DICE__NUM_ROLLS__24WORD)])
else:
entropy = ''.join([str(random.randint(1, 6)) for i in range(mnemonic_generation.DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else mnemonic_generation.DICE__NUM_ROLLS__24WORD)])
elif method == 'coins':
entropy = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)])
elif method == 'final_word':
random_dice_rolls = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)])
entropy = " ".join(mnemonic_generation.generate_mnemonic_from_coin_flips(random_dice_rolls)[:-1])
print(len(entropy.split()), entropy)
if method == 'dice':
if not zero_indexed_dice and ('0' in entropy or '6' not in entropy):
print("Dice entry must be 1-6 unless --zero-indexed-dice is specified")
exit(1)
if len(entropy) not in [mnemonic_generation.DICE__NUM_ROLLS__12WORD, mnemonic_generation.DICE__NUM_ROLLS__24WORD]:
print(f"Dice entropy must be {mnemonic_generation.DICE__NUM_ROLLS__12WORD} or {mnemonic_generation.DICE__NUM_ROLLS__24WORD} rolls")
exit(1)
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(entropy)
elif method == 'coins':
if len(entropy) not in [128, 256]:
print("Coin flip entropy must be 128 or 256 flips")
exit(1)
mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(entropy)
elif method == 'final_word':
num_input_words = len(entropy.split())
if num_input_words not in [11, 12, 23, 24]:
print(f"Final word entropy must be 11, 12, 23, or 24 words ({num_input_words} provided)")
exit(1)
if num_input_words in [11, 23]:
# We need to fill the last bits of entropy
if num_input_words == 11:
# 7 final bits of entropy in the 12th word (7 + 4-bit checksum)
num_final_entropy_bits = 7
else:
# 3 final bits of entropy in the 24th word (3 + 8-bit checksum)
num_final_entropy_bits = 3
final_entropy_method = None
while final_entropy_method not in ['1', '2', '3']:
final_entropy_method = input(f"""
How would you like to fill the final {num_final_entropy_bits} bits of entropy?
1.) {num_final_entropy_bits} coin flips
2.) Select an additional word from the wordlist
3.) Fill with zeros
Type 1, 2, or 3: """)
if final_entropy_method == '1':
coin_flips = input(f""" Enter {num_final_entropy_bits} coin flips as 0 or 1 (e.g. { "".join(str(random.randint(0,1)) for f in range(0, num_final_entropy_bits)) }): """)
if len(coin_flips) != num_final_entropy_bits:
print(f"Invalid number of coin flips: needed {num_final_entropy_bits}, got {len(coin_flips)}")
final_word = mnemonic_generation.get_partial_final_word(coin_flips)
entropy += f" {final_word}"
elif final_entropy_method == '2':
final_word = input(f""" Enter the final word: """)
if final_word not in WORDLIST__ENGLISH:
print(f"Invalid word: {final_word}")
exit(1)
entropy += f" {final_word}"
elif final_entropy_method == '3':
# Nothing to do; just pass the 11 or 23 words straight to calculate_checksum
pass
mnemonic = mnemonic_generation.calculate_checksum(entropy)
print("\n")
if is_rand_seed:
print("\t***** This is a demo seed. Do not use it to store funds!!! *****")
print(f"""\t{" ".join(mnemonic)}\n""")
if is_rand_seed:
print(f"\tEntropy: {entropy}\n")
if method == "dice":
print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Base 10" or "Hex" mode.\n""")
elif method == "coins":
print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Binary" mode.\n""")