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
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:
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:
build:
name: build
runs-on: ubuntu-latest
# Prevent resource consuming cron triggered runs in forks
if: (!github.event.repository.fork || github.event_name == 'workflow_dispatch')
strategy:
fail-fast: false
matrix:
@@ -16,25 +34,41 @@ jobs:
uses: actions/checkout@v3
with:
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
path: "seedsigner-os"
- name: get seedsigner-os latest commit hash
id: get-seedsigner-os-hash
run: |
cd seedsigner-os
echo "builder_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
# get full history + tags for "git describe"
fetch-depth: 0
- name: checkout source
uses: actions/checkout@v3
with:
# ref defaults to repo default-branch=dev (cron) or SHA of event (workflow_dispatch)
path: "seedsigner-os/opt/rootfs-overlay/opt"
# get full history + tags for "git describe"
fetch-depth: 0
- name: get seedsigner latest commit hash
id: get-seedsigner-hash
- name: Get and set meta data
run: |
git init
echo "source_hash=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV
# The builder_hash (seedsigner-os hash) for the cache action step key
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
run: |
@@ -44,11 +78,9 @@ jobs:
ls -la src
- name: restore build cache
id: build-cache-restore
uses: actions/cache/restore@v3
# Caching seedsigner-os/buildroot_dl is optional.
# Caching it can save a small amount of build time,
# but it will occupy a larger amount of storage space.
uses: actions/cache@v3
# Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour,
# while consuming ~850 MB storage space).
with:
path: |
~/.buildroot-ccache/
@@ -62,16 +94,6 @@ jobs:
cd seedsigner-os/opt
./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)
run: |
ls -la seedsigner-os/images
@@ -79,7 +101,7 @@ jobs:
- name: rename image
run: |
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
run: |
@@ -96,6 +118,8 @@ jobs:
name: seedsigner_os_images
path: "seedsigner-os/images/*.img"
if-no-files-found: error
# maximum 90 days retention
retention-days: 90
sha256sum:
name: calculate sha256sum
@@ -128,4 +152,6 @@ jobs:
with:
name: seedsigner_os_images
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)
* [Enclosure Designs](#enclosure-designs)
* [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
[![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.
Additional information about the project can be found at [SeedSigner.com](https://seedsigner.com).
@@ -81,9 +85,11 @@ Notes:
# Software Installation
## 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
@@ -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.
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:
@@ -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)
---------------
# Manual Installation Instructions
see the docs: [Manual Installation Instructions](docs/manual_installation.md)
# Build from Source
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.
---
# 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:
@@ -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):
```bash
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
```
+2
View File
@@ -1,3 +1,5 @@
picamera==1.13
# numpy = transitive picamera dependency
numpy==1.25.2
RPi.GPIO==0.7.0
spidev==3.5
-2
View File
@@ -1,7 +1,5 @@
embit==0.7.0
numpy==1.25.2
Pillow==9.4.0
pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03
qrcode==7.3.1
six==1.16.0
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.
self.screensaver.start()
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):
+2
View File
@@ -29,6 +29,8 @@ class GUIConstants:
BITCOIN_ORANGE = "#FF9416"
TESTNET_COLOR = "#00F100"
REGTEST_COLOR = "#00CAF1"
GREEN_INDICATOR_COLOR = "#00FF00"
INACTIVE_COLOR = "#414141"
ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900"
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.camera import Camera
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 ..components import GUIConstants, Fonts, TextArea
from .screen import BaseScreen
from ..components import GUIConstants, Fonts, SeedSignerIconConstants
@@ -47,6 +47,9 @@ class ScanScreen(BaseScreen):
framerate: int = 6 # TODO: alternate optimization for Pi Zero 2W?
render_rect: tuple[int,int,int,int] = None
FRAME__ADDED_PART = 1
FRAME__REPEATED_PART = 2
FRAME__MISS = 3
def __post_init__(self):
from seedsigner.hardware.camera import Camera
@@ -58,17 +61,22 @@ class ScanScreen(BaseScreen):
self.camera = Camera.get_instance()
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(
camera=self.camera,
decoder=self.decoder,
renderer=self.renderer,
instructions_text=self.instructions_text,
render_rect=self.render_rect,
frame_decode_status=self.frames_decode_status,
frames_decoded_counter=self.frames_decoded_counter,
))
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.decoder = decoder
self.renderer = renderer
@@ -77,6 +85,9 @@ class ScanScreen(BaseScreen):
self.render_rect = render_rect
else:
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_height = self.render_rect[3] - self.render_rect[1]
self.decoder_fps = "0.0"
@@ -85,12 +96,15 @@ class ScanScreen(BaseScreen):
def run(self):
from timeit import default_timer as timer
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()
num_frames = 0
debug = False
show_framerate = False # enable for debugging / testing
while self.keep_running:
frame = self.camera.read_video_stream(as_image=True)
@@ -98,16 +112,22 @@ class ScanScreen(BaseScreen):
num_frames += 1
cur_time = time.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"
if show_framerate:
scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}"
else:
scan_text = None
progress_percentage = self.decoder.get_percent_complete()
if progress_percentage == 0:
# We've just started scanning, no results yet
if show_framerate:
scan_text = f"{cur_fps:0.2f} | {self.decoder_fps}"
else:
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:
if frame.width > self.render_width or frame.height > self.render_height:
frame = frame.resize(
@@ -115,14 +135,13 @@ class ScanScreen(BaseScreen):
resample=Image.NEAREST # Use nearest neighbor for max speed
)
draw = ImageDraw.Draw(frame)
if scan_text:
# Note: shadowed text (adding a 'stroke' outline) can
# significantly slow down the rendering.
# Temp solution: render a slight 1px shadow behind the text
# TODO: Replace the instructions_text with a disappearing
# toast/popup (see: QR Brightness UI)?
draw = ImageDraw.Draw(frame)
draw.text(xy=(
int(self.renderer.canvas_width/2 + 2),
self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 2
@@ -142,8 +161,82 @@ class ScanScreen(BaseScreen):
font=instructions_font,
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)
# print(f" {cur_fps:0.2f} | {self.decoder_fps}")
if self.camera._video_stream is None:
break
@@ -169,6 +262,21 @@ class ScanScreen(BaseScreen):
if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID):
self.camera.stop_video_stream_mode()
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):
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.renderer import Renderer
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
from seedsigner.models.encode_qr import BaseQrEncoder
from seedsigner.models.settings import SettingsConstants
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
@@ -657,10 +658,10 @@ class LargeButtonScreen(BaseTopNavScreen):
@dataclass
class QRDisplayScreen(BaseScreen):
qr_encoder: 'EncodeQR' = None
qr_encoder: BaseQrEncoder = None
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):
super().__init__()
self.qr_encoder = qr_encoder
+1 -1
View File
@@ -38,7 +38,7 @@ class Camera(Singleton):
return frame
else:
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
+71 -12
View File
@@ -2,21 +2,40 @@ import hashlib
import unicodedata
from embit import bip39
from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes
from typing import List
from seedsigner.models.settings_definition import SettingsConstants
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.
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
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]:
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]:
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]:
return bip39.mnemonic_from_bytes(entropy_bytes).split()
def generate_mnemonic_from_bytes(entropy_bytes, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
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()
if len(roll_data) == 50:
if len(roll_data) == DICE__NUM_ROLLS__12WORD:
# 12-word mnemonic; only use 128bits / 16 bytes
entropy_bytes = entropy_bytes[:16]
# 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
# 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
hash = hashlib.sha256(image.tobytes())
# 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.
# Licensed under the "BSD-2-Clause Plus Patent License"
#
import time
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
@@ -71,13 +71,53 @@ class FountainDecoder:
def result_error(self):
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():
return 1
if self.expected_part_indexes == None:
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):
# Don't process the part if we're already done
@@ -93,6 +133,9 @@ class FountainDecoder:
self.last_part_indexes = p.indexes
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
while not self.is_complete() and len(self.queued_parts) != 0:
self.process_queue_item()
@@ -101,6 +144,12 @@ class FountainDecoder:
self.processed_parts_count += 1
# 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
@@ -114,6 +163,7 @@ class FountainDecoder:
self.queued_parts.append(p)
def process_queue_item(self):
start = time.time()
part = self.queued_parts.pop(0)
# self.print_part(part)
@@ -121,6 +171,8 @@ class FountainDecoder:
self.process_simple_part(part)
else:
self.process_mixed_part(part)
# print(f"Queue processing: {int((time.time() - start)*1000.0)}ms")
# self.print_state()
def reduce_mixed_by(self, p):
@@ -141,6 +193,7 @@ class FountainDecoder:
new_mixed[reduced_part.indexes] = reduced_part
self.mixed_parts = new_mixed
# print(self.mixed_parts.keys())
def reduce_part_by_part(self, a, b):
# 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))
def print_state(self):
parts = self.expected_part_count() if self.expected_part_indexes != None else 'None'
received = self.indexes_to_string(self.received_part_indexes)
guesstimate = self.estimated_percent_complete(weight_mixed_frames=True)
original_metric = self.estimated_percent_complete()
mixed = []
for indexes, p in self.mixed_parts.items():
mixed.append(self.indexes_to_string(indexes))
mixed_s = "[{}]".format(', '.join(mixed))
queued = len(self.queued_parts)
res = self.result_description()
print('parts: {}, received: {}, mixed: {}, queued: {}, result: {}'.format(parts, received, mixed_s, queued, res))
mixed_set = set()
try:
for indexes, p in self.mixed_parts.items():
if not indexes or len(indexes) == 0:
continue
mixed.append(self.indexes_to_string(indexes))
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.fragments = FountainEncoder.partition_message(message, self.fragment_len)
self.seq_num = first_seq_num
self.current_part: Part = None
@staticmethod
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)
mixed = self.mix(indexes)
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):
result = [0] * self.fragment_len
+2 -2
View File
@@ -153,8 +153,8 @@ class URDecoder:
def processed_parts_count(self):
return self.fountain_decoder.processed_parts_count
def estimated_percent_complete(self):
return self.fountain_decoder.estimated_percent_complete()
def estimated_percent_complete(self, weight_mixed_frames: bool = False):
return self.fountain_decoder.estimated_percent_complete(weight_mixed_frames=weight_mixed_frames)
def is_success(self):
result = self.result
+16 -2
View File
@@ -34,12 +34,26 @@ class UREncoder:
def is_single_part(self):
return self.fountain_encoder.is_single_part()
def next_part(self):
part = self.fountain_encoder.next_part()
def next_part(self) -> str:
if self.is_single_part():
return UREncoder.encode(self.ur)
else:
part = self.fountain_encoder.next_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
def encode_part(type, part):
+15 -11
View File
@@ -120,11 +120,14 @@ class DecodeQR:
qr_str = data
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():
self.complete = True
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:
# All other formats use the same method signature
@@ -219,12 +222,12 @@ class DecodeQR:
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:
return 0
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]:
if self.decoder.total_segments == None:
@@ -261,6 +264,7 @@ class DecodeQR:
QRType.PSBT__BASE43,
]
@property
def is_seed(self):
return self.qr_type in [
@@ -305,7 +309,7 @@ class DecodeQR:
@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:
return None
@@ -337,10 +341,10 @@ class DecodeQR:
# PSBT
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
return QRType.PSBT__UR2
elif re.search("^UR:CRYPTO-OUTPUT/", s, re.IGNORECASE):
return QRType.OUTPUT__UR
elif re.search("^UR:CRYPTO-ACCOUNT/", s, re.IGNORECASE):
return QRType.ACCOUNT__UR
@@ -362,10 +366,10 @@ class DecodeQR:
elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', desc_str, re.IGNORECASE):
# if json starting with label and contains descriptor, assume specter wallet json
return QRType.WALLET__SPECTER
elif "multisig setup file" in s.lower():
return QRType.WALLET__CONFIGFILE
elif "sortedmulti" in s:
return QRType.WALLET__GENERIC
@@ -392,7 +396,7 @@ class DecodeQR:
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
except:
_4LETTER_WORDLIST = []
if all(x in wordlist for x in s.strip().split(" ")):
# checks if all words in list are in bip39 word list
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
# below.
pass
# Is it byte data?
# 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR
if len(s) == 32 or len(s) == 16:
+217 -358
View File
@@ -2,7 +2,7 @@ import math
from embit import bip32
from embit.networks import NETWORKS
from binascii import b2a_base64, hexlify
from binascii import hexlify
from dataclasses import dataclass
from typing import List
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 import UR
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.settings import SettingsConstants
@@ -21,149 +19,62 @@ from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIP
@dataclass
class EncodeQR:
"""
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
class BaseQrEncoder:
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):
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
def is_complete(self):
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):
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):
def seq_len(self):
return 1
def cur_part(self) -> str:
""" static QRs only have a single part, which `next_part` always returns """
return self.next_part()
@property
@@ -172,86 +83,125 @@ class BaseStaticQrEncoder(BaseQrEncoder):
class BasePsbtQrEncoder(BaseQrEncoder):
def __init__(self, psbt: PSBT):
self.psbt = psbt
@dataclass
class SeedQrEncoder(BaseStaticQrEncoder):
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):
def __init__(self, psbt, qr_density):
super().__init__(psbt)
self.qr_max_fragment_size = 20
@dataclass
class CompactSeedQrEncoder(SeedQrEncoder):
def next_part(self):
# 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())
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)
# Must return data as `bytes` for `qrcode` to properly recognize it as byte data
return bytes(as_bytes)
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
def is_complete(self):
return self.ur2_encode.is_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
return self.sent_complete
def seq_len(self):
@@ -275,154 +225,34 @@ class SpecterPsbtQrEncoder(BasePsbtQrEncoder):
return part
@property
def is_complete(self):
return self.sent_complete
def cur_part(self) -> str:
if self.part_num_sent == 0:
# 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()
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 = []
def restart(self) -> str:
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
def is_complete(self):
return self.sent_complete
class SpecterXPubQrEncoder(XpubQrEncoder):
def __init__(self, qr_density, **kwargs):
# 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 qr_max_fragment_size(self):
density_mapping = {
SettingsConstants.DENSITY__LOW: 40,
SettingsConstants.DENSITY__MEDIUM: 65,
SettingsConstants.DENSITY__HIGH: 90,
}
return density_mapping.get(self.qr_density, 65)
def _create_parts(self):
self.parts = []
self.prep_xpub()
start = 0
stop = self.qr_max_fragment_size
qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1
@@ -442,34 +272,60 @@ class SpecterXPubQrEncoder(XpubQrEncoder):
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:
# if part num sent is gt number of parts, start at 0
if self.part_num_sent > (len(self.parts) - 1):
self.part_num_sent = 0
return self.ur2_encode.next_part().upper()
part = self.parts[self.part_num_sent]
# when parts sent eq num of parts in list
if self.part_num_sent == (len(self.parts) - 1):
self.sent_complete = True
def cur_part(self) -> str:
return self.ur2_encode.current_part().upper()
# increment to next part
self.part_num_sent += 1
return part
def restart(self):
self.ur2_encode.fountain_encoder.restart()
class UrXpubQrEncoder(XpubQrEncoder):
def __init__(self, qr_density, **kwargs):
super().__init__(**kwargs)
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
@dataclass
class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder):
def __post_init__(self):
super().__post_init__()
self.prep_xpub()
def derivation_to_keypath(path: str) -> list:
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)
def seq_len(self):
return self.ur2_encode.fountain_encoder.seq_len()
@dataclass
class UrPsbtQrEncoder(BaseFountainQrEncoder):
psbt: PSBT = None
def next_part(self) -> str:
return self.ur2_encode.next_part().upper()
def __post_init__(self):
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,
passphrase: str = "",
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:
raise Exception("Must initialize a Seed with a mnemonic List[str]")
@@ -58,7 +58,10 @@ class Seed:
@property
def mnemonic_list(self) -> List[str]:
return self._mnemonic
@property
def wordlist_language_code(self) -> str:
return self._wordlist_language_code
@property
def mnemonic_display_str(self) -> str:
@@ -95,7 +98,7 @@ class Seed:
@property
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):
+1 -1
View File
@@ -430,7 +430,7 @@ class SettingsDefinition:
type=SettingsConstants.TYPE__MULTISELECT,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
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,
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.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.qr_type import QRType
from seedsigner.models.settings import SettingsConstants
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)
@@ -300,6 +299,7 @@ class PSBTAddressDetailsView(View):
class PSBTChangeDetailsView(View):
NEXT = "Next"
SKIP_VERIFICATION = "Skip Verificiation"
VERIFY_MULTISIG = "Verify Multisig Change"
@@ -360,7 +360,7 @@ class PSBTChangeDetailsView(View):
else:
# 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:
# Single sig
@@ -428,7 +428,8 @@ class PSBTChangeDetailsView(View):
if selected_menu_num == RET_CODE__BACK_BUTTON:
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:
return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1})
else:
@@ -513,11 +514,9 @@ class PSBTFinalizeView(View):
class PSBTSignedQRDisplayView(View):
def run(self):
qr_encoder = EncodeQR(
qr_encoder = UrPsbtQrEncoder(
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),
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
)
self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder)
+4
View File
@@ -47,6 +47,10 @@ class ScanView(View):
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
if self.decoder.is_complete:
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.helpers import embit_utils
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.qr_type import QRType
from seedsigner.models.seed import InvalidSeedException, Seed
@@ -754,20 +754,25 @@ class SeedExportXpubCoordinatorView(View):
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]
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(
ButtonListScreen,
title="Export Xpub",
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)):
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:
if selected_menu_num == RET_CODE__BACK_BUTTON:
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):
@@ -898,35 +903,24 @@ class SeedExportXpubQRDisplayView(View):
super().__init__()
self.seed = self.controller.get_seed(seed_num)
qr_density = self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY)
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(
encoder_args = dict(
seed=self.seed,
derivation=derivation_path,
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
qr_type=qr_type,
qr_density=qr_density,
wordlist_language_code=self.seed.wordlist_language_code,
sig_type=sig_type
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY)
)
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):
self.run_screen(
@@ -1426,11 +1420,12 @@ class SeedTranscribeSeedQRWholeQRView(View):
def run(self):
e = EncodeQR(
seed=self.seed,
qr_type=self.seedqr_format,
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
)
encoder_args = dict(seed=self.seed)
if self.seedqr_format == QRType.SEED__SEEDQR:
e = SeedQrEncoder(**encoder_args)
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
e = CompactSeedQrEncoder(**encoder_args)
data = e.next_part()
ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen(
@@ -1461,11 +1456,12 @@ class SeedTranscribeSeedQRZoomedInView(View):
def run(self):
e = EncodeQR(
seed=self.seed,
qr_type=self.seedqr_format,
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
)
encoder_args = dict(seed=self.seed)
if self.seedqr_format == QRType.SEED__SEEDQR:
e = SeedQrEncoder(**encoder_args)
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
e = CompactSeedQrEncoder(**encoder_args)
data = e.next_part()
if len(self.seed.mnemonic_list) == 24:
@@ -2108,7 +2104,7 @@ class SeedSignMessageSignedMessageQRView(View):
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(
QRDisplayScreen,
+6 -7
View File
@@ -14,8 +14,7 @@ from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen,
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen)
from seedsigner.helpers import embit_utils, mnemonic_generation
from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.qr_type import QRType
from seedsigner.models.encode_qr import GenericStaticQrEncoder
from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView, SeedExportXpubScriptTypeView
@@ -188,8 +187,8 @@ class ToolsImageEntropyMnemonicLengthView(View):
****************************************************************************"""
class ToolsDiceEntropyMnemonicLengthView(View):
def run(self):
TWELVE = "12 words (50 rolls)"
TWENTY_FOUR = "24 words (99 rolls)"
TWELVE = f"12 words ({mnemonic_generation.DICE__NUM_ROLLS__12WORD} rolls)"
TWENTY_FOUR = f"24 words ({mnemonic_generation.DICE__NUM_ROLLS__24WORD} rolls)"
button_data = [TWELVE, TWENTY_FOUR]
selected_menu_num = ButtonListScreen(
@@ -203,10 +202,10 @@ class ToolsDiceEntropyMnemonicLengthView(View):
return Destination(BackStackView)
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:
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):
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(
QRDisplayScreen,
qr_encoder=qr_encoder,
+1 -1
View File
@@ -1,6 +1,6 @@
import sys
from dataclasses import dataclass
from mock import MagicMock, Mock, patch
from unittest.mock import MagicMock, Mock, patch
from typing import Callable
# Prevent importing modules w/Raspi hardware dependencies.
+2 -3
View File
@@ -1,4 +1,3 @@
coverage==7.2.1
mock==4.0.3
pytest==6.2.4
coverage==7.3.1
pytest==7.4.2
pytest-cov==4.1.0
+1 -1
View File
@@ -2,7 +2,7 @@ import embit
import os
import sys
import time
from mock import Mock, patch, MagicMock
from unittest.mock import Mock, patch, MagicMock
from seedsigner.helpers import embit_utils
from seedsigner.models.settings import Settings
+1 -1
View File
@@ -1,5 +1,5 @@
import pytest
from mock import MagicMock
from unittest.mock import MagicMock
from seedsigner.models.seed import Seed
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__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__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__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
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 seedsigner.controller import Controller
from seedsigner.views.view import MainMenuView
from seedsigner.views import scan_views, seed_views, psbt_views
from seedsigner.models.settings import SettingsConstants
@@ -74,7 +73,7 @@ class TestPSBTFlows(FlowTest):
FlowStep(psbt_views.PSBTOverviewView),
FlowStep(psbt_views.PSBTMathView),
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.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_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.PSBTMathView),
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.PSBTSignedQRDisplayView),
FlowStep(MainMenuView),
+1 -1
View File
@@ -1,7 +1,7 @@
import os
from typing import Callable
from mock import PropertyMock, patch
from unittest.mock import PropertyMock, patch
# Must import test base before the Controller
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
from base import FlowTest, FlowStep
+40 -17
View File
@@ -12,16 +12,17 @@ def test_dice_rolls():
dice_rolls = ""
for i in range(0, 99):
# 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)
assert len(mnemonic) == 24
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
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
dice_rolls += str(random.randint(0, 5))
dice_rolls += str(random.randint(1, 6))
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
assert len(mnemonic) == 12
@@ -29,18 +30,40 @@ def test_dice_rolls():
def test_calculate_checksum():
""" Given an 11-word or 23-word mnemonic, the calculated checksum should yield a
def test_calculate_checksum_input_type():
"""
Given an 11-word or 23-word mnemonic, the calculated checksum should yield a
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/
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"
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
_try_all_input_formats(partial_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"
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
_try_all_input_formats(partial_mnemonic)
def test_calculate_checksum_invalid_mnemonics():
@@ -50,25 +73,25 @@ def test_calculate_checksum_invalid_mnemonics():
with pytest.raises(Exception) as e:
# Mnemonic is too short: 10 words instead of 11
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)
with pytest.raises(Exception) as e:
# Valid mnemonic but unsupported length
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)
with pytest.raises(Exception) as e:
# Mnemonic is too short: 22 words instead of 23
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)
with pytest.raises(ValueError) as e:
# Invalid BIP-39 word
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)
@@ -78,17 +101,17 @@ def test_calculate_checksum_with_default_final_word():
the mnemonic.
"""
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"
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
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"
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
+9 -10
View File
@@ -1,10 +1,8 @@
import os
import pyzbar
from embit import bip39
from seedsigner.helpers.qr import QR
from seedsigner.helpers.ur2.bytewords import decode
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.seed import Seed
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):
""" Helper method to re-run multiple variations of the same encode/decode test """
print(entropy)
seed_phrase = bip39.mnemonic_from_bytes(entropy).split()
print(seed_phrase)
assert len(seed_phrase) == mnemonic_length
mnemonic = bip39.mnemonic_from_bytes(entropy).split()
assert len(mnemonic) == 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()
print(data)
@@ -35,8 +35,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
assert status == DecodeQRStatus.COMPLETE
decoded_seed_phrase = decoder.get_seed_phrase()
print(decoded_seed_phrase)
assert seed_phrase == decoded_seed_phrase
assert mnemonic == 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""")