From f741fd2fa36e6ec468f383f7fb9509699f5bca9a Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Thu, 14 Sep 2023 22:16:45 -0400 Subject: [PATCH 01/49] Add CI Status badge to README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1cbfa827..a64e04ff 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ # Project Summary +[![CI](https://github.com/SeedSigner/seedsigner/actions/workflows/tests.yml/badge.svg)](https://github.com/SeedSigner/seedsigner/actions/workflows/tests.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). From 06330905c3d8df3f8e50f60176f4dacba852ba87 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 15 Sep 2023 18:35:24 -0500 Subject: [PATCH 02/49] fix xpub export coordinator selection --- src/seedsigner/views/seed_views.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 15c3fde9..850e2119 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -718,20 +718,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): From 44498d88658c75ae44b74579f66953f5b4cbe829 Mon Sep 17 00:00:00 2001 From: Daniel Bast <2790401+dbast@users.noreply.github.com> Date: Sat, 16 Sep 2023 09:24:14 +0200 Subject: [PATCH 03/49] Use mock from Python stdlib and upgrade test requirements versions mock is part of the Python standard lib since Python 3.3, see also https://docs.python.org/3/library/unittest.mock.html. Switching to that allows to remove it from `test/requirements.txt`. While at it, also updating the package versions in `test/requirements.txt`. --- tests/base.py | 2 +- tests/requirements.txt | 5 ++--- tests/screenshot_generator/generator.py | 2 +- tests/test_bip85.py | 2 +- tests/test_flows_settings.py | 2 +- tests/test_flows_view.py | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/base.py b/tests/base.py index 496e1b86..e7bb2ddf 100644 --- a/tests/base.py +++ b/tests/base.py @@ -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. diff --git a/tests/requirements.txt b/tests/requirements.txt index a66aac6b..d42adea7 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -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 diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 83a58135..a2ce87c5 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -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 diff --git a/tests/test_bip85.py b/tests/test_bip85.py index 5ade3917..737be569 100644 --- a/tests/test_bip85.py +++ b/tests/test_bip85.py @@ -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 diff --git a/tests/test_flows_settings.py b/tests/test_flows_settings.py index a6eb34d9..c449f679 100644 --- a/tests/test_flows_settings.py +++ b/tests/test_flows_settings.py @@ -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 diff --git a/tests/test_flows_view.py b/tests/test_flows_view.py index 6d032460..695ee093 100644 --- a/tests/test_flows_view.py +++ b/tests/test_flows_view.py @@ -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 From df7e1cba148598a0b5584ef5c6890e4c7dacb50f Mon Sep 17 00:00:00 2001 From: 3rd Iteration Date: Sat, 17 Feb 2024 11:28:54 -0500 Subject: [PATCH 04/49] Fix Always Wiping Multisig Descriptor at Main Menu --- src/seedsigner/controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 206ad2c0..18cdfb72 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -298,7 +298,6 @@ class Controller(Singleton): # Home always wipes the back_stack/state of temp vars self.resume_main_flow = None - self.multisig_wallet_descriptor = None self.unverified_address = None self.address_explorer_data = None self.psbt = None From fca41a27063c46d02901073821aad94290c48e69 Mon Sep 17 00:00:00 2001 From: CryptoGuide Date: Sun, 18 Feb 2024 13:44:27 -0500 Subject: [PATCH 05/49] Change text & add warning when signing a Multisig transaction without a descriptor loaded. --- src/seedsigner/views/psbt_views.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 9c0d9c60..7d99a8af 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -285,6 +285,7 @@ class PSBTAddressDetailsView(View): class PSBTChangeDetailsView(View): NEXT = "Next" + SKIP_VERIFICATION = "Skip Verificiation" VERIFY_MULTISIG = "Verify Multisig Change" @@ -330,9 +331,11 @@ class PSBTChangeDetailsView(View): if is_change_derivation_path: title = "Your Change" self.VERIFY_MULTISIG = "Verify Multisig Change" + unverified_warning_text = "Can't Verify that Change Outputs Belong to your Wallet" else: title = "Self-Transfer" self.VERIFY_MULTISIG = "Verify Multisig Addr" + unverified_warning_text = "Can't Verify that Self-Transfer Outputs Belong to your Wallet" # if psbt_parser.num_change_outputs > 1: # title += f" (#{self.change_address_num + 1})" @@ -345,7 +348,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 @@ -413,7 +416,19 @@ 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 button_data[selected_menu_num] == self.SKIP_VERIFICATION: + self.run_screen( + DireWarningScreen, + title="Security Warning", + status_icon_name=SeedSignerIconConstants.WARNING, + status_headline="Potential Loss of Funds", + text=unverified_warning_text, + ) + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + 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: From b29f1114c03e379aa4d23db4b94853951d418385 Mon Sep 17 00:00:00 2001 From: CryptoGuide Date: Sun, 18 Feb 2024 13:53:13 -0500 Subject: [PATCH 06/49] Fix menu navigation --- src/seedsigner/views/psbt_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 7d99a8af..d6a75186 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -418,7 +418,7 @@ class PSBTChangeDetailsView(View): elif button_data[selected_menu_num] == self.NEXT or button_data[selected_menu_num] == self.SKIP_VERIFICATION: if button_data[selected_menu_num] == self.SKIP_VERIFICATION: - self.run_screen( + selected_menu_num = self.run_screen( DireWarningScreen, title="Security Warning", status_icon_name=SeedSignerIconConstants.WARNING, From 66d68047fb55bfecf6ea90dd757740f6c7074e93 Mon Sep 17 00:00:00 2001 From: CryptoGuide Date: Sun, 18 Feb 2024 14:28:22 -0500 Subject: [PATCH 07/49] Fix up CI tests to handle new warning screen --- tests/test_flows_psbt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py index 02b71446..c5e0f72a 100644 --- a/tests/test_flows_psbt.py +++ b/tests/test_flows_psbt.py @@ -73,7 +73,8 @@ 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.PSBTChangeDetailsView, button_data_selection=0), 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), @@ -85,7 +86,8 @@ 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.PSBTChangeDetailsView, button_data_selection=0), FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT), FlowStep(psbt_views.PSBTSignedQRDisplayView), FlowStep(MainMenuView), From 2e3c74e015d7598e84897a7c8992029cc667dca1 Mon Sep 17 00:00:00 2001 From: 3rd Iteration Date: Tue, 20 Feb 2024 22:44:18 -0500 Subject: [PATCH 08/49] Revert "Fix Always Wiping Multisig Descriptor at Main Menu" This reverts commit df7e1cba148598a0b5584ef5c6890e4c7dacb50f. --- src/seedsigner/controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 18cdfb72..206ad2c0 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -298,6 +298,7 @@ class Controller(Singleton): # Home always wipes the back_stack/state of temp vars self.resume_main_flow = None + self.multisig_wallet_descriptor = None self.unverified_address = None self.address_explorer_data = None self.psbt = None From 591a1b0b9d28cbbe30dad56a4405d1bbd53dfaaf Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 21:58:41 -0500 Subject: [PATCH 09/49] Entropy-to-mnemonic CLI utility * Makes our existing mnemonic_generation.oy accessible as an interactive CLI utility for converting dice rolls or coin flips into 12- or 24-word mnemonics as well as the final word calculation. * Reduces python dependencies in mnemonic_generation.py * Removes need to always specify `wordlist_language_code`. * Adds constants for number of dice rolls for each mnemonic phrase length (since 99 may become 100 soon). * Generates test dice rolls, coin flips, or word selections via its "rand12" and "rand24" option. * Adds support for coin flips, even though the SeedSigner UI does not support coin flips yet (and might not ever). * Updates dice_verification.md accordingly. --- docs/dice_verification.md | 54 ++++ src/seedsigner/helpers/mnemonic_generation.py | 284 +++++++++++++++++- src/seedsigner/views/tools_views.py | 8 +- tests/test_mnemonic_generation.py | 57 ++-- 4 files changed, 370 insertions(+), 33 deletions(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index 361bf14d..d5a23cb7 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -205,3 +205,57 @@ 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. +### SeedSigner verification script +_(for more advanced/python-savvy users)_ + +Install the `embit` dependency: +``` +pip3 install embit +``` + +Then run the utility script with `-h` to view the usage instructions: +``` +cd src/seedsigner/helpers +python3 mnemonic_generation.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_generation.py dice 5624433434... + + # 99 dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice 6151463561... + + # 50 dice rolls, entered as 0-5 / 12-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + + # 128 coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins 1111100111... + + # 256 coin flips / 24-word mnemonic + python mnemonic_generation.py coins 0010111010... + + # GENERATE 50 random dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice rand12 + + # GENERATE 99 random dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice rand24 + + # GENERATE 99 random dice rolls, entered as 0-5 / 24-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + + # GENERATE 128 random coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins rand12 + + # GENERATE 256 random coin flips / 24-word mnemonic + python3 mnemonic_generation.py coins rand24 +``` + + +### Epilogue +You can use these methods to do dry run time to time to verify that no one has changed the micro sdcard. But do not use the generated 24 words as a valid wallet, they need to be generated alone, only on the seedsigner! diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index a021a834..9a7caa63 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -1,22 +1,76 @@ import hashlib +import random import unicodedata from embit import bip39 from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes -from typing import List +from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH + +""" + This is SeedSigner's internal mnemonic generation utility but is also meant to + function as an independently-executable CLI to facilitate external verification of + SeedSigner's mnemonic generation for a given input entropy. + + Therefore its module imports should be kept to the bare minimum. + + ## Running as a standalone script + Install the `embit` library: + ``` + pip3 install embit + ``` + + And then run: + ``` + python3 mnemonic_generation.py -h + ``` +""" + + +# Hard-coded value from SettingsConstants to avoid dependencies +WORDLIST_LANGUAGE__ENGLISH = 'en' + +DICE__NUM_ROLLS__12WORD = 50 +DICE__NUM_ROLLS__24WORD = 99 + + +def _get_wordlist(wordlist_language_code) -> list[str]: + """ + Convenience method to fetch the wordlist for the given language code without + requiring any SeedSigner module dependencies for when this is run as a + standalone CLI. + """ + if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: + return WORDLIST__ENGLISH + else: + # Nested import to avoid dependency on Seed model when running this script standalone + from seedsigner.models import Seed + return Seed.get_wordlist(wordlist_language_code) -def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]: +def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = 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]) + if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: + temp_final_word = "abandon" + else: + # Nested import to avoid dependency on Seed model when running this script standalone + from seedsigner.models import Seed + 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") @@ -27,7 +81,7 @@ def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str] # Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation # because we assume it's incorrect since we either let the user select their own # final word OR we injected the 0000 word from the wordlist. - mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code)) + mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=_get_wordlist(wordlist_language_code)) # This function will convert the bytes back into a mnemonic, but it will also # calculate the proper checksum bits while doing so. For a 12-word seed it will just @@ -37,28 +91,234 @@ 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 = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_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 = 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=_get_wordlist(wordlist_language_code)).split() + + + +def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: str = 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=_get_wordlist(wordlist_language_code)).split() + + + +def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> str: + """ Look up the partial final word for the given coin flips. + 7 coin flips: 0101010 + 0000 where the final 4 bits will be replaced with the checksum + 3 coin flips: 0101 + 0000000 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) + + if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: + wordlist = WORDLIST__ENGLISH + else: + from seedsigner.models import Seed, SettingsConstants + wordlist = Seed.get_wordlist(wordlist_language_code) + + return wordlist[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 = 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=_get_wordlist(wordlist_language_code)).split() + + + +if __name__ == "__main__": + import argparse + + 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: + # {DICE__NUM_ROLLS__12WORD} dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice 5624433434... + + # {DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice 6151463561... + + # {DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + + # 128 coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins 1111100111... + + # 256 coin flips / 24-word mnemonic + python mnemonic_generation.py coins 0010111010... + + # GENERATE {DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice rand12 + + # GENERATE {DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice rand24 + + # GENERATE {DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + + # GENERATE 128 random coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins rand12 + + # GENERATE 256 random coin flips / 24-word mnemonic + python3 mnemonic_generation.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(DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else DICE__NUM_ROLLS__24WORD)]) + else: + entropy = ''.join([str(random.randint(1, 6)) for i in range(DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else 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(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 [DICE__NUM_ROLLS__12WORD, DICE__NUM_ROLLS__24WORD]: + print(f"Dice entropy must be {DICE__NUM_ROLLS__12WORD} or {DICE__NUM_ROLLS__24WORD} rolls") + exit(1) + mnemonic = 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 = 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 = 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 = 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""") \ No newline at end of file diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 12a4493b..b94c9b46 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -188,8 +188,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 +203,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)) diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index 76298d34..000ce4d7 100644 --- a/tests/test_mnemonic_generation.py +++ b/tests/test_mnemonic_generation.py @@ -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.split(" ")) 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.split(" ")) 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.split(" ")) 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.split(" ")) 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.split(" ")) 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.split(" ")) 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.split(" ")) 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.split(" ")) assert mnemonic1 == mnemonic2 From 752d5c404bf8c14ab44da6321cbe934ac40be149 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 22:00:14 -0500 Subject: [PATCH 10/49] Update dice_verification.md --- docs/dice_verification.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index d5a23cb7..de699feb 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -205,8 +205,9 @@ 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. -### SeedSigner verification script +### 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. Install the `embit` dependency: ``` From 1d399b577d9b053b503ec578e6f325e401960adc Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 22:05:14 -0500 Subject: [PATCH 11/49] cleanup --- src/seedsigner/helpers/mnemonic_generation.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 9a7caa63..9a96be2b 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -63,13 +63,7 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = WORDL mnemonic = re.findall(r'[^,\s]+', mnemonic) if len(mnemonic) in [11, 23]: - if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: - temp_final_word = "abandon" - else: - # Nested import to avoid dependency on Seed model when running this script standalone - from seedsigner.models import Seed - temp_final_word = Seed.get_wordlist(wordlist_language_code)[0] - + temp_final_word = _get_wordlist(wordlist_language_code)[0] mnemonic.append(temp_final_word) if len(mnemonic) not in [12, 24]: From b04ef6dc33f66288f4b0c77d2f38a09eb41b2166 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 22:07:59 -0500 Subject: [PATCH 12/49] cleanup --- src/seedsigner/helpers/mnemonic_generation.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 9a96be2b..c35ea684 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -138,14 +138,7 @@ def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLI binary_string = coin_flips + "0" * (11 - len(coin_flips)) wordlist_index = int(binary_string, 2) - if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: - wordlist = WORDLIST__ENGLISH - else: - from seedsigner.models import Seed, SettingsConstants - wordlist = Seed.get_wordlist(wordlist_language_code) - - return wordlist[wordlist_index] - + return _get_wordlist(wordlist_language_code)[wordlist_index] From 9b640ade2d509a773bde3594b0eb72595857d355 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 22:12:23 -0500 Subject: [PATCH 13/49] cleanup --- tests/test_mnemonic_generation.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index 000ce4d7..a1f46e85 100644 --- a/tests/test_mnemonic_generation.py +++ b/tests/test_mnemonic_generation.py @@ -73,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(" ")) + 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(" ")) + 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(" ")) + 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(" ")) + mnemonic_generation.calculate_checksum(partial_mnemonic) assert "not in the dictionary" in str(e) @@ -101,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(" ")) + mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic) partial_mnemonic += " abandon" - mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + 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(" ")) + mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic) partial_mnemonic += " abandon" - mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic) assert mnemonic1 == mnemonic2 From d94ffb58c8511e388801c08ba2e35177c692c816 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 7 Aug 2023 10:08:08 -0500 Subject: [PATCH 14/49] Remove Epilogue that had already been edited out --- docs/dice_verification.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index de699feb..93a14be0 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -256,7 +256,3 @@ python3 mnemonic_generation.py -h # GENERATE 256 random coin flips / 24-word mnemonic python3 mnemonic_generation.py coins rand24 ``` - - -### Epilogue -You can use these methods to do dry run time to time to verify that no one has changed the micro sdcard. But do not use the generated 24 words as a valid wallet, they need to be generated alone, only on the seedsigner! From f696ec2fd85bbe93d0fe04800f02fc5ece22774e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 7 Aug 2023 10:29:47 -0500 Subject: [PATCH 15/49] Update dice_verification.md --- docs/dice_verification.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index 93a14be0..e36e4c1d 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -205,8 +205,11 @@ 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 +--- + +# 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. Install the `embit` dependency: From 2a15925aea4481f533c7a1552da0233f3f979af3 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 7 Aug 2023 19:58:14 -0500 Subject: [PATCH 16/49] How to verify in iancoleman.io --- docs/dice_verification.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index e36e4c1d..79ca82cb 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -259,3 +259,12 @@ python3 mnemonic_generation.py -h # GENERATE 256 random coin flips / 24-word mnemonic python3 mnemonic_generation.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]" From 6fb8a351c421927680b07fd16cd1532e20e4a3cb Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 11 Aug 2023 08:43:57 -0500 Subject: [PATCH 17/49] comment correction --- src/seedsigner/helpers/mnemonic_generation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index c35ea684..04b5d598 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -132,8 +132,8 @@ def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: s def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> str: """ Look up the partial final word for the given coin flips. - 7 coin flips: 0101010 + 0000 where the final 4 bits will be replaced with the checksum - 3 coin flips: 0101 + 0000000 where the final 8 bits will be replaced with the checksum + 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) From 9535f21a7ed744ae891bac69c55de591acf9e2d8 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 28 Jan 2024 10:54:03 -0600 Subject: [PATCH 18/49] refactor script into tools/ --- docs/dice_verification.md | 14 +- src/seedsigner/helpers/mnemonic_generation.py | 159 ---------------- tools/mnemonic.py | 169 ++++++++++++++++++ 3 files changed, 178 insertions(+), 164 deletions(-) create mode 100644 tools/mnemonic.py diff --git a/docs/dice_verification.md b/docs/dice_verification.md index 79ca82cb..bc26c6eb 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -212,15 +212,19 @@ _(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. -Install the `embit` dependency: -``` +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: -``` -cd src/seedsigner/helpers -python3 mnemonic_generation.py -h +```bash +cd tools +python3 mnemonic.py -h ``` ``` diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 04b5d598..2cc95950 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -1,9 +1,7 @@ import hashlib -import random import unicodedata from embit import bip39 -from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH """ @@ -152,160 +150,3 @@ def generate_mnemonic_from_image(image, wordlist_language_code: str = WORDLIST_L return bip39.mnemonic_from_bytes(hash.digest(), wordlist=_get_wordlist(wordlist_language_code)).split() - -if __name__ == "__main__": - import argparse - - 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: - # {DICE__NUM_ROLLS__12WORD} dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice 5624433434... - - # {DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice 6151463561... - - # {DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... - - # 128 coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins 1111100111... - - # 256 coin flips / 24-word mnemonic - python mnemonic_generation.py coins 0010111010... - - # GENERATE {DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice rand12 - - # GENERATE {DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice rand24 - - # GENERATE {DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice rand24 - - # GENERATE 128 random coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins rand12 - - # GENERATE 256 random coin flips / 24-word mnemonic - python3 mnemonic_generation.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(DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else DICE__NUM_ROLLS__24WORD)]) - else: - entropy = ''.join([str(random.randint(1, 6)) for i in range(DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else 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(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 [DICE__NUM_ROLLS__12WORD, DICE__NUM_ROLLS__24WORD]: - print(f"Dice entropy must be {DICE__NUM_ROLLS__12WORD} or {DICE__NUM_ROLLS__24WORD} rolls") - exit(1) - mnemonic = 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 = 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 = 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 = 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""") \ No newline at end of file diff --git a/tools/mnemonic.py b/tools/mnemonic.py new file mode 100644 index 00000000..3e46f792 --- /dev/null +++ b/tools/mnemonic.py @@ -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_generation.py dice 5624433434... + + # {mnemonic_generation.DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice 6151463561... + + # {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + + # 128 coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins 1111100111... + + # 256 coin flips / 24-word mnemonic + python mnemonic_generation.py coins 0010111010... + + # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice rand12 + + # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice rand24 + + # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + + # GENERATE 128 random coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins rand12 + + # GENERATE 256 random coin flips / 24-word mnemonic + python3 mnemonic_generation.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""") From 1ed703b8bc3d44948b4bc39200628bb92a97b7c8 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 28 Jan 2024 11:00:37 -0600 Subject: [PATCH 19/49] cleanup --- src/seedsigner/helpers/mnemonic_generation.py | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 2cc95950..aa124a33 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -3,41 +3,29 @@ import unicodedata from embit import bip39 from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH +from seedsigner.models.settings_definition import SettingsConstants """ - This is SeedSigner's internal mnemonic generation utility but is also meant to - function as an independently-executable CLI to facilitate external verification of - SeedSigner's mnemonic generation for a given input entropy. + 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. - Therefore its module imports should be kept to the bare minimum. - - ## Running as a standalone script - Install the `embit` library: - ``` - pip3 install embit - ``` - - And then run: - ``` - python3 mnemonic_generation.py -h - ``` + see: docs/dice_verification.md (the "Command Line Tool" section). """ - -# Hard-coded value from SettingsConstants to avoid dependencies -WORDLIST_LANGUAGE__ENGLISH = 'en' - DICE__NUM_ROLLS__12WORD = 50 DICE__NUM_ROLLS__24WORD = 99 + def _get_wordlist(wordlist_language_code) -> list[str]: """ Convenience method to fetch the wordlist for the given language code without requiring any SeedSigner module dependencies for when this is run as a standalone CLI. """ - if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: + if wordlist_language_code == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH: return WORDLIST__ENGLISH else: # Nested import to avoid dependency on Seed model when running this script standalone @@ -46,7 +34,7 @@ def _get_wordlist(wordlist_language_code) -> list[str]: -def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> 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. @@ -83,12 +71,12 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = WORDL -def generate_mnemonic_from_bytes(entropy_bytes, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> 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, wordlist=_get_wordlist(wordlist_language_code)).split() -def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> 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. @@ -109,7 +97,7 @@ def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = WO -def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: +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. @@ -128,7 +116,7 @@ def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: s -def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> str: +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 @@ -142,11 +130,9 @@ def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLI # 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, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> 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(), wordlist=_get_wordlist(wordlist_language_code)).split() - - From b29e0bcf50f4d4a94a17ab30b29a493d73cabb8c Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 2 Mar 2024 07:56:49 -0600 Subject: [PATCH 20/49] Simplification; no longer need to reduce imports --- src/seedsigner/helpers/mnemonic_generation.py | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index aa124a33..8a560049 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -4,6 +4,7 @@ import unicodedata from embit import bip39 from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH from seedsigner.models.settings_definition import SettingsConstants +from seedsigner.models.seed import Seed """ This is SeedSigner's internal mnemonic generation utility. @@ -19,21 +20,6 @@ DICE__NUM_ROLLS__24WORD = 99 -def _get_wordlist(wordlist_language_code) -> list[str]: - """ - Convenience method to fetch the wordlist for the given language code without - requiring any SeedSigner module dependencies for when this is run as a - standalone CLI. - """ - if wordlist_language_code == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH: - return WORDLIST__ENGLISH - else: - # Nested import to avoid dependency on Seed model when running this script standalone - from seedsigner.models import Seed - return Seed.get_wordlist(wordlist_language_code) - - - 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. @@ -49,7 +35,7 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = Setti mnemonic = re.findall(r'[^,\s]+', mnemonic) if len(mnemonic) in [11, 23]: - temp_final_word = _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]: @@ -61,7 +47,7 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = Setti # Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation # because we assume it's incorrect since we either let the user select their own # final word OR we injected the 0000 word from the wordlist. - mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=_get_wordlist(wordlist_language_code)) + mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code)) # This function will convert the bytes back into a mnemonic, but it will also # calculate the proper checksum bits while doing so. For a 12-word seed it will just @@ -72,7 +58,7 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = Setti 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=_get_wordlist(wordlist_language_code)).split() + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() @@ -93,7 +79,7 @@ def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = Se entropy_bytes = entropy_bytes[:16] # Return as a list - return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_get_wordlist(wordlist_language_code)).split() + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() @@ -112,7 +98,7 @@ def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: s entropy_bytes = entropy_bytes[:16] # Return as a list - return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_get_wordlist(wordlist_language_code)).split() + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() @@ -124,7 +110,7 @@ def get_partial_final_word(coin_flips: str, wordlist_language_code: str = Settin binary_string = coin_flips + "0" * (11 - len(coin_flips)) wordlist_index = int(binary_string, 2) - return _get_wordlist(wordlist_language_code)[wordlist_index] + return Seed.get_wordlist(wordlist_language_code)[wordlist_index] @@ -135,4 +121,4 @@ def generate_mnemonic_from_image(image, wordlist_language_code: str = SettingsCo hash = hashlib.sha256(image.tobytes()) # Return as a list - return bip39.mnemonic_from_bytes(hash.digest(), wordlist=_get_wordlist(wordlist_language_code)).split() + return bip39.mnemonic_from_bytes(hash.digest(), wordlist=Seed.get_wordlist(wordlist_language_code)).split() From 5160418449ed1a7ce8b9143406431aa97d59bf70 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 2 Mar 2024 07:57:39 -0600 Subject: [PATCH 21/49] Docs fixup after moving CLI to its own file --- docs/dice_verification.md | 20 ++++++++++---------- tools/mnemonic.py | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index bc26c6eb..7e3833b4 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -234,34 +234,34 @@ python3 mnemonic.py -h Usage: # 50 dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice 5624433434... + python3 mnemonic.py dice 5624433434... # 99 dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice 6151463561... + python3 mnemonic.py dice 6151463561... # 50 dice rolls, entered as 0-5 / 12-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + python3 mnemonic.py --zero-indexed-dice dice 5135535514... # 128 coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins 1111100111... + python3 mnemonic.py coins 1111100111... # 256 coin flips / 24-word mnemonic - python mnemonic_generation.py coins 0010111010... + python mnemonic.py coins 0010111010... # GENERATE 50 random dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice rand12 + python3 mnemonic.py dice rand12 # GENERATE 99 random dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice rand24 + python3 mnemonic.py dice rand24 # GENERATE 99 random dice rolls, entered as 0-5 / 24-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + python3 mnemonic.py --zero-indexed-dice dice rand24 # GENERATE 128 random coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins rand12 + python3 mnemonic.py coins rand12 # GENERATE 256 random coin flips / 24-word mnemonic - python3 mnemonic_generation.py coins rand24 + python3 mnemonic.py coins rand24 ``` ### How to get the same results in iancoleman.io diff --git a/tools/mnemonic.py b/tools/mnemonic.py index 3e46f792..28490e94 100644 --- a/tools/mnemonic.py +++ b/tools/mnemonic.py @@ -21,34 +21,34 @@ 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_generation.py dice 5624433434... + python3 mnemonic.py dice 5624433434... # {mnemonic_generation.DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice 6151463561... + python3 mnemonic.py dice 6151463561... # {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + python3 mnemonic.py --zero-indexed-dice dice 5135535514... # 128 coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins 1111100111... + python3 mnemonic.py coins 1111100111... # 256 coin flips / 24-word mnemonic - python mnemonic_generation.py coins 0010111010... + python mnemonic.py coins 0010111010... # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice rand12 + python3 mnemonic.py dice rand12 # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice rand24 + python3 mnemonic.py dice rand24 # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + python3 mnemonic.py --zero-indexed-dice dice rand24 # GENERATE 128 random coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins rand12 + python3 mnemonic.py coins rand12 # GENERATE 256 random coin flips / 24-word mnemonic - python3 mnemonic_generation.py coins rand24 + python3 mnemonic.py coins rand24 """ RAND_12 = "rand12" RAND_24 = "rand24" From c4aafd0bf32bf245d636f18efdebaeafd1fce5ce Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 2 Mar 2024 07:59:16 -0600 Subject: [PATCH 22/49] minor further import cleanup --- src/seedsigner/helpers/mnemonic_generation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 8a560049..b3e90680 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -2,7 +2,6 @@ import hashlib import unicodedata from embit import bip39 -from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH from seedsigner.models.settings_definition import SettingsConstants from seedsigner.models.seed import Seed From c570285e9425bd174c84e5f739b297acc0f6bce0 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 15 Sep 2023 18:35:24 -0500 Subject: [PATCH 23/49] fix xpub export coordinator selection --- src/seedsigner/views/seed_views.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index afd70e10..5426ee64 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -718,20 +718,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): From 165f7b2a2724baa79cad50e96b21fe91af20a7b7 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 15 Sep 2023 18:15:12 -0500 Subject: [PATCH 24/49] interim commit --- src/seedsigner/gui/screens/screen.py | 10 +- src/seedsigner/helpers/qr_encoders.py | 443 ++++++++++++++ .../helpers/ur2/fountain_encoder.py | 4 +- src/seedsigner/helpers/ur2/ur_encoder.py | 14 +- src/seedsigner/models/encode_qr.py | 538 ------------------ src/seedsigner/views/psbt_views.py | 7 +- src/seedsigner/views/seed_views.py | 64 +-- src/seedsigner/views/tools_views.py | 5 +- tests/test_encodepsbtqr.py | 74 +-- tests/test_flows_psbt.py | 1 - tests/test_seedqr.py | 20 +- 11 files changed, 529 insertions(+), 651 deletions(-) create mode 100644 src/seedsigner/helpers/qr_encoders.py delete mode 100644 src/seedsigner/models/encode_qr.py diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index b229857d..ca9da429 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -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.helpers.qr_encoders 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 @@ -751,12 +752,15 @@ class QRDisplayScreen(BaseScreen): while self.keep_running: # convert the self.qr_brightness integer (31-255) into hex triplets hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3 - image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color) # Display the brightness tips toast duration = 10 ** 9 * 1.2 # 1.2 seconds if show_brightness_tips and time.time_ns() - self.tips_start_time.cur_count < duration: + image = self.qr_encoder.part_to_image(self.qr_encoder.cur_part(), 240, 240, border=2, background_color=hex_color) self.add_brightness_tips(image) + else: + # Only advance the QR animation when the brightness tip is not displayed + image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color) with self.renderer.lock: self.renderer.show_image(image) diff --git a/src/seedsigner/helpers/qr_encoders.py b/src/seedsigner/helpers/qr_encoders.py new file mode 100644 index 00000000..0a72c764 --- /dev/null +++ b/src/seedsigner/helpers/qr_encoders.py @@ -0,0 +1,443 @@ +import math + +from embit import bip32 +from embit.networks import NETWORKS +from binascii import b2a_base64, hexlify +from dataclasses import dataclass +from typing import List +from embit import bip32 +from embit.networks import NETWORKS +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.models.qr_type import QRType +from seedsigner.models.seed import Seed +from seedsigner.models.settings import SettingsConstants + +from urtypes.crypto import PSBT as UR_PSBT +from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP + + + +@dataclass +class BaseQrEncoder: + qr_density: str = SettingsConstants.DENSITY__MEDIUM + + + def __post_init__(self): + print("BaseQrEncoder.__post_init__") + self.qr = QR() + + + @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 _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"): + # 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) + 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 + def is_complete(self): + return True + + + +@dataclass +class SeedQrEncoder(BaseStaticQrEncoder): + mnemonic: List[str] = None + wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH + + def __post_init__(self): + self.wordlist = Seed.get_wordlist(self.wordlist_language_code) + super().__post_init__() + + self.data = "" + # Output as Numeric data format + for word in self.mnemonic: + index = self.wordlist.index(word) + self.data += str("%04d" % index) + + + def next_part(self): + return self.data + + + +@dataclass +class CompactSeedQrEncoder(SeedQrEncoder): + def next_part(self): + # Output as binary data format + binary_str = "" + for word in self.mnemonic: + 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.mnemonic) == 24: + # 8 checksum bits in a 24-word seed + binary_str = binary_str[:-8] + + elif len(self.mnemonic) == 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) + + + +@dataclass +class GenericStaticQrEncoder(BaseStaticQrEncoder): + data: str = None + + def next_part(self): + return self.data + + + +@dataclass +class BaseXpubQrEncoder(BaseQrEncoder): + """ + Base Xpub QrEncoder for static and animated formats + """ + seed_phrase: list = None + passphrase: str = None + derivation: str = None + network: str = SettingsConstants.MAINNET + wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH + + def prep_xpub(self): + print("BaseXpubQrEncoder.prep_xpub") + self.wordlist = Seed.get_wordlist(self.wordlist_language_code) + + 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.seed = Seed(mnemonic=self.seed_phrase, + passphrase=self.passphrase, + wordlist_language_code=self.wordlist_language_code) + 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.sent_complete + + + def seq_len(self): + return len(self.parts) + + + 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 + + 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 + + # increment to next part + self.part_num_sent += 1 + + return part + + + 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() + + + +@dataclass +class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder): + @property + 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.prep_xpub() + start = 0 + stop = self.qr_max_fragment_size + qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1 + + if qr_cnt == 1: + self.parts.append(self.xpubstring[start:stop]) + + cnt = 0 + while cnt < qr_cnt and qr_cnt != 1: + part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + self.xpubstring[start:stop] + self.parts.append(part) + + start = start + self.qr_max_fragment_size + stop = stop + self.qr_max_fragment_size + if stop > len(self.xpubstring): + stop = len(self.xpubstring) + cnt += 1 + + + +# !! This format is no longer used !! +# @dataclass +# class SpecterPsbtQrEncoder(BaseSimpleAnimatedQREncoder, BaseQrEncoder): +# psbt: PSBT +# qr_density: str + +# def __post_init__(self): +# super().__post_init__() +# if self.qr_density == SettingsConstants.DENSITY__LOW: +# self.qr_max_fragement_size = 40 +# elif self.qr_density == SettingsConstants.DENSITY__MEDIUM: +# self.qr_max_fragement_size = 65 +# elif self.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 + + + +"""************************************************************************************** + Fountain encoded animated QR encoders +**************************************************************************************""" +@dataclass +class BaseFountainQrEncoder(BaseQrEncoder): + def __post_init__(self): + super().__post_init__() + print("BaseFountainQrEncoder __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: + return self.ur2_encode.next_part().upper() + + + def cur_part(self) -> str: + return self.ur2_encode.current_part().upper() + + + +@dataclass +class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder): + def __post_init__(self): + super().__post_init__() + print("UrXpubQrEncoder __post_init__") + + self.prep_xpub() + + def derivation_to_keypath(path: str) -> list: + arr = path.split("/") + if arr[0] == "m": + arr = arr[1:] + if len(arr) == 0: + return Keypath([],self.root.my_fingerprint, None) + if arr[-1] == "": + # trailing slash + arr = arr[:-1] + + for i, e in enumerate(arr): + if e[-1] == "h" or e[-1] == "'": + arr[i] = PathComponent(int(e[:-1]), True) + else: + arr[i] = PathComponent(int(e), False) + + return Keypath(arr, self.root.my_fingerprint, len(arr)) + + origin = derivation_to_keypath(self.derivation) + + self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(), + 'chain_code': self.xpub.chain_code, + 'origin': origin, + 'parent_fingerprint': self.xpub.fingerprint}) + + ur_outputs = [] + + if len(origin.components) > 0: + if origin.components[0].index == 84: # Native Single Sig + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) + elif origin.components[0].index == 49: # Nested Single Sig + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) + elif origin.components[0].index == 48: # Multisig + if len(origin.components) >= 4: + if origin.components[3].index == 2: # Native Multisig + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) + elif origin.components[3].index == 1: # Nested Multisig + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) + elif origin.components[0].index == 86: # P2TR + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[409]],self.ur_hdkey)) + + # If empty, add all script types + if len(ur_outputs) == 0: + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[403]],self.ur_hdkey)) + + ur_account = Account(self.root.my_fingerprint, ur_outputs) + + qr_ur_bytes = UR("crypto-account", ur_account.to_cbor()) + + self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size) + + + +@dataclass +class UrPsbtQrEncoder(BaseFountainQrEncoder): + psbt: PSBT = None + + 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) diff --git a/src/seedsigner/helpers/ur2/fountain_encoder.py b/src/seedsigner/helpers/ur2/fountain_encoder.py index ac43bb62..45888efd 100644 --- a/src/seedsigner/helpers/ur2/fountain_encoder.py +++ b/src/seedsigner/helpers/ur2/fountain_encoder.py @@ -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,8 @@ 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 mix(self, indexes): result = [0] * self.fragment_len diff --git a/src/seedsigner/helpers/ur2/ur_encoder.py b/src/seedsigner/helpers/ur2/ur_encoder.py index ac1cbc7d..f0836413 100644 --- a/src/seedsigner/helpers/ur2/ur_encoder.py +++ b/src/seedsigner/helpers/ur2/ur_encoder.py @@ -34,12 +34,22 @@ 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) + @staticmethod def encode_part(type, part): diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py deleted file mode 100644 index 6efa715d..00000000 --- a/src/seedsigner/models/encode_qr.py +++ /dev/null @@ -1,538 +0,0 @@ -import math - -from embit import bip32 -from embit.networks import NETWORKS -from binascii import b2a_base64, hexlify -from dataclasses import dataclass -from typing import List -from embit import bip32 -from embit.networks import NETWORKS -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.models.qr_type import QRType -from seedsigner.models.seed import Seed -from seedsigner.models.settings import SettingsConstants - -from urtypes.crypto import PSBT as UR_PSBT -from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP - - - - -@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_phrase: List[str] = None - passphrase: str = None - derivation: str = None - network: str = SettingsConstants.MAINNET - qr_type: str = None - qr_density: str = SettingsConstants.DENSITY__MEDIUM - wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH - bitcoin_address: str = None - signed_message: str = None - - 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_phrase=self.seed_phrase, - passphrase=self.passphrase, - derivation=self.derivation, - network=self.network, - wordlist_language_code=self.wordlist_language_code - ) - - elif self.qr_type == QRType.XPUB__UR: - self.encoder = UrXpubQrEncoder( - qr_density=self.qr_density, - seed_phrase=self.seed_phrase, - passphrase=self.passphrase, - derivation=self.derivation, - network=self.network, - wordlist_language_code=self.wordlist_language_code - ) - - elif self.qr_type == QRType.XPUB__SPECTER: - self.encoder = SpecterXPubQrEncoder( - qr_density=self.qr_density, - seed_phrase=self.seed_phrase, - passphrase=self.passphrase, - derivation=self.derivation, - network=self.network, - wordlist_language_code=self.wordlist_language_code - ) - - - # SeedQR formats - elif self.qr_type == QRType.SEED__SEEDQR: - self.encoder = SeedQrEncoder(seed_phrase=self.seed_phrase, - wordlist_language_code=self.wordlist_language_code) - - elif self.qr_type == QRType.SEED__COMPACTSEEDQR: - self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase, - 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") - - def _create_parts(self): - raise Exception("Not implemented in child class") - - - -class BaseStaticQrEncoder(BaseQrEncoder): - def seq_len(self): - return 1 - - - @property - def is_complete(self): - return True - - - -class BasePsbtQrEncoder(BaseQrEncoder): - def __init__(self, psbt: PSBT): - self.psbt = psbt - - - -class UrPsbtQrEncoder(BasePsbtQrEncoder): - def __init__(self, psbt, qr_density): - super().__init__(psbt) - self.qr_max_fragment_size = 20 - - 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) - - - def seq_len(self): - return self.ur2_encode.fountain_encoder.seq_len() - - - def next_part(self) -> str: - return self.ur2_encode.next_part().upper() - - - @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 - - - def seq_len(self): - return len(self.parts) - - - 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 - - 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 - - # increment to next part - self.part_num_sent += 1 - - return part - - - @property - def is_complete(self): - return self.sent_complete - - - -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_phrase, passphrase, derivation, network, wordlist_language_code): - self.seed_phrase = seed_phrase - self.passphrase = passphrase - self.derivation = derivation - self.network = network - self.wordlist = Seed.get_wordlist(wordlist_language_code) - self.parts = [] - self.part_num_sent = 0 - self.sent_complete = False - - 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.seed = Seed(mnemonic=self.seed_phrase, - passphrase=self.passphrase, - wordlist_language_code=wordlist_language_code) - 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) - - - @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 _create_parts(self): - self.parts = [] - - start = 0 - stop = self.qr_max_fragment_size - qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1 - - if qr_cnt == 1: - self.parts.append(self.xpubstring[start:stop]) - - cnt = 0 - while cnt < qr_cnt and qr_cnt != 1: - part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + self.xpubstring[start:stop] - self.parts.append(part) - - start = start + self.qr_max_fragment_size - stop = stop + self.qr_max_fragment_size - if stop > len(self.xpubstring): - stop = len(self.xpubstring) - cnt += 1 - - - 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 - - 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 - - # increment to next part - self.part_num_sent += 1 - - return part - - - -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 - - def derivation_to_keypath(path: str) -> list: - arr = path.split("/") - if arr[0] == "m": - arr = arr[1:] - if len(arr) == 0: - return Keypath([],self.root.my_fingerprint, None) - if arr[-1] == "": - # trailing slash - arr = arr[:-1] - - for i, e in enumerate(arr): - if e[-1] == "h" or e[-1] == "'": - arr[i] = PathComponent(int(e[:-1]), True) - else: - arr[i] = PathComponent(int(e), False) - - return Keypath(arr, self.root.my_fingerprint, len(arr)) - - origin = derivation_to_keypath(self.derivation) - - self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(), - 'chain_code': self.xpub.chain_code, - 'origin': origin, - 'parent_fingerprint': self.xpub.fingerprint}) - - ur_outputs = [] - - if len(origin.components) > 0: - if origin.components[0].index == 84: # Native Single Sig - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) - elif origin.components[0].index == 49: # Nested Single Sig - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) - elif origin.components[0].index == 48: # Multisig - if len(origin.components) >= 4: - if origin.components[3].index == 2: # Native Multisig - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) - elif origin.components[3].index == 1: # Nested Multisig - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) - elif origin.components[0].index == 86: # P2TR - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[409]],self.ur_hdkey)) - - # If empty, add all script types - if len(ur_outputs) == 0: - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[403]],self.ur_hdkey)) - - ur_account = Account(self.root.my_fingerprint, ur_outputs) - - qr_ur_bytes = UR("crypto-account", ur_account.to_cbor()) - - 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() - - - def next_part(self) -> str: - return self.ur2_encode.next_part().upper() diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 9c0d9c60..fdb94473 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -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.helpers.qr_encoders 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) @@ -498,11 +497,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) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 5426ee64..183b1a36 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -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.helpers.qr_encoders 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 @@ -863,35 +863,25 @@ 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_phrase=self.seed.mnemonic_list, passphrase=self.seed.passphrase, 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 + 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( @@ -1391,11 +1381,13 @@ class SeedTranscribeSeedQRWholeQRView(View): def run(self): - e = EncodeQR( - seed_phrase=self.seed.mnemonic_list, - qr_type=self.seedqr_format, - wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) - ) + encoder_args = dict(mnemonic=self.seed.mnemonic_list, + wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + 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( @@ -1426,11 +1418,13 @@ class SeedTranscribeSeedQRZoomedInView(View): def run(self): - e = EncodeQR( - seed_phrase=self.seed.mnemonic_list, - qr_type=self.seedqr_format, - wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) - ) + encoder_args = dict(seed_phrase=self.seed.mnemonic_list, + wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + 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: @@ -2071,7 +2065,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, diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 12a4493b..47b71fa1 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -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.helpers.qr_encoders 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 @@ -695,7 +694,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, diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 75ed796f..c1765355 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -1,5 +1,4 @@ -from seedsigner.models.encode_qr import EncodeQR -from seedsigner.models.qr_type import QRType +from seedsigner.helpers.qr_encoders import CompactSeedQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder from embit import psbt from binascii import a2b_base64 @@ -7,75 +6,47 @@ from seedsigner.models.settings import SettingsConstants -def test_ur_qr_encode(): +def test_ur_psbt_qr_encode(): base64_psbt = "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" tx = psbt.PSBT.parse(a2b_base64(base64_psbt)) - e = EncodeQR(psbt=tx, qr_type=QRType.PSBT__UR2) + e = UrPsbtQrEncoder(psbt=tx, qr_density=SettingsConstants.DENSITY__MEDIUM) cnt = 0 while cnt <= 10: fragment = e.next_part() - img = e.part_to_image(fragment) - cnt += 1 - - - -def test_specter_qr_encode(): - base64_psbt = "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" - - tx = psbt.PSBT.parse(a2b_base64(base64_psbt)) - - e = EncodeQR(psbt=tx, qr_type=QRType.PSBT__SPECTER) - - cnt = 0 - while cnt <= 10: - fragment = e.next_part() - if (cnt+1) == 1: - assert fragment == "p1of117 cHNidP8BAIkCAAAAAaLlQ/VRNpx3IFtoRTOCnq2xfJwg/n7R9XB0TTTnlX/UHQAAA" - elif (cnt+1) == 2: - assert fragment == "p2of117 AD9////AtzQAwAAAAAAIgAgCwVSg4Ae1lGNHzy76jLN6GSQaSVnktnmNDByu/wkn7" - elif (cnt+1) == 3: - assert fragment == "p3of117 FQwwAAAAAAACIAIJyFZJe7xxQjXpoEBhb8mIkau9OhobDS7xbYxnRIjJUSAAAAAE8" - elif (cnt+1) == 4: - assert fragment == "p4of117 BBIiyHgQFgrfagAAAAqP8rWjHFRBmTEWK39AFjd6Wo1sw1UxlgIvROVHUOHbiAzre" - elif (cnt+1) == 5: - assert fragment == "p5of117 +t61zOqKFV1xXtDPuUcQRh3M92zh0Zar8rDLPJKQFH7fnFkwAACAAAAAgAAAAIACA" - elif (cnt+1) == 6: - assert fragment == "p6of117 ACATwEEiLIeBFIg7+eAAAACubwMfJNby3zfn9owhFfgl/Xe/GiHciMMxxB9v6q7BW" - elif (cnt+1) == 7: - assert fragment == "p7of117 cCurV9rH+K8ucVU3w52mcEttDldz7kh5cS0xBtWs7wmTYU4IEbazAAAIAAAACAAAA" - elif (cnt+1) == 8: - assert fragment == "p8of117 AgAIAAIBPAQSIsh4EX+8GLoAAAALvSlncnGchVCfK7tnzHPVYcBRcck0JGQuspGFp" - elif (cnt+1) == 9: - assert fragment == "p9of117 cGP+YQIAXYODa8PIF3hOOnUeYHhlv4PQ+UZCYynQCOoKgVJRhhQYTQfrMAAAgAAAA" - elif (cnt+1) == 10: - assert fragment == "p10of117 IAAAACAAgAAgE8BBIiyHgRgkAVVgAAAAsgLKl/ahhLHvS/3Cth+9Hde12MHJO5PP8" - elif (cnt+1) == 11: - assert fragment == "p11of117 REKtbWkqONAvETqIlMPWJ/f1uBvSCGFm+zzDYnnEBtuAYjZiQrzj9mFLQz4JUwAAC" - - img = e.part_to_image(fragment) + e.part_to_image(fragment, 240, 240) cnt += 1 def test_seedsigner_qr(): - mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + # test vector 1 from the SeedQR docs + mnemonic = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split() + e = SeedQrEncoder(mnemonic=mnemonic) + assert e.next_part() == "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643" - e = EncodeQR(seed_phrase=mnemonic.split(), qr_type=QRType.SEED__SEEDQR) + e = CompactSeedQrEncoder(mnemonic=mnemonic) + assert e.next_part() == b'\x0et\xb6A\x07\xf9L\xc0\xcc\xfa\xe6\xa1=\xcb\xec6b\x15O\xecg\xe0\xe0\t\x99\xc0x\x92Y}\x19\n' - print(e.next_part()) + # test vector 4 from the SeedQR docs + mnemonic="forum undo fragile fade shy sign arrest garment culture tube off merit".split() + e = SeedQrEncoder(mnemonic=mnemonic) + assert e.next_part() == "073318950739065415961602009907670428187212261116" - assert e.next_part() == "121802020768124106400009195602431595117715840445" + e = CompactSeedQrEncoder(mnemonic=mnemonic) + assert e.next_part() == b'[\xbd\x9dq\xa8\xecy\x90\x83\x1a\xff5\x9dBeE' def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(), passphrase="pass", qr_type=QRType.XPUB, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h") - + e = StaticXpubQrEncoder(seed_phrase=mnemonic.split(), + passphrase="pass", + derivation="m/48h/1h/0h/2h", + network=SettingsConstants.TESTNET) assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5" @@ -83,7 +54,7 @@ def test_xpub_qr(): def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUB__SPECTER, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + e = SpecterXPubQrEncoder(seed_phrase=mnemonic.split(" "), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" @@ -95,10 +66,9 @@ def test_specter_xpub_qr(): def test_ur_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR( + e = UrXpubQrEncoder( seed_phrase=mnemonic.split(), passphrase="pass", - qr_type=QRType.XPUB__UR, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__MEDIUM diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py index 02b71446..93d4847d 100644 --- a/tests/test_flows_psbt.py +++ b/tests/test_flows_psbt.py @@ -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 diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 7e2d6385..472af661 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -1,23 +1,22 @@ 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.helpers.qr_encoders import SeedQrEncoder, CompactSeedQrEncoder from seedsigner.models.qr_type import QRType -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(mnemonic=mnemonic) + elif qr_type == QRType.SEED__COMPACTSEEDQR: + e = CompactSeedQrEncoder(mnemonic=mnemonic) - e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type) data = e.next_part() print(data) @@ -34,8 +33,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 From 7f11bfc8133c87321e0b54ba9b50d4662031f5d1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Sep 2023 19:23:42 -0500 Subject: [PATCH 25/49] Fountain encoder `restart`; cleanup --- src/seedsigner/helpers/qr_encoders.py | 58 ++++++--------------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/src/seedsigner/helpers/qr_encoders.py b/src/seedsigner/helpers/qr_encoders.py index 0a72c764..5dda6978 100644 --- a/src/seedsigner/helpers/qr_encoders.py +++ b/src/seedsigner/helpers/qr_encoders.py @@ -46,6 +46,10 @@ class BaseQrEncoder: 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") @@ -242,6 +246,10 @@ class BaseSimpleAnimatedQREncoder(BaseQrEncoder): return self.next_part() + def restart(self) -> str: + self.part_num_sent = 0 + + @dataclass class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder): @@ -277,52 +285,6 @@ class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder): -# !! This format is no longer used !! -# @dataclass -# class SpecterPsbtQrEncoder(BaseSimpleAnimatedQREncoder, BaseQrEncoder): -# psbt: PSBT -# qr_density: str - -# def __post_init__(self): -# super().__post_init__() -# if self.qr_density == SettingsConstants.DENSITY__LOW: -# self.qr_max_fragement_size = 40 -# elif self.qr_density == SettingsConstants.DENSITY__MEDIUM: -# self.qr_max_fragement_size = 65 -# elif self.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 - - - """************************************************************************************** Fountain encoded animated QR encoders **************************************************************************************""" @@ -365,6 +327,10 @@ class BaseFountainQrEncoder(BaseQrEncoder): def cur_part(self) -> str: return self.ur2_encode.current_part().upper() + + + def restart(self): + self.ur2_encode.fountain_encoder.restart() From fdbe7777446d148b4b98c22ed58cd8289bf672b3 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Sep 2023 19:24:18 -0500 Subject: [PATCH 26/49] Fountain encoder `restart` --- src/seedsigner/helpers/ur2/fountain_encoder.py | 9 +++++++++ src/seedsigner/helpers/ur2/ur_encoder.py | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/src/seedsigner/helpers/ur2/fountain_encoder.py b/src/seedsigner/helpers/ur2/fountain_encoder.py index 45888efd..af5dd338 100644 --- a/src/seedsigner/helpers/ur2/fountain_encoder.py +++ b/src/seedsigner/helpers/ur2/fountain_encoder.py @@ -146,6 +146,15 @@ class FountainEncoder: data = bytes(mixed) 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 diff --git a/src/seedsigner/helpers/ur2/ur_encoder.py b/src/seedsigner/helpers/ur2/ur_encoder.py index f0836413..082ad17b 100644 --- a/src/seedsigner/helpers/ur2/ur_encoder.py +++ b/src/seedsigner/helpers/ur2/ur_encoder.py @@ -49,6 +49,10 @@ class UREncoder: 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 From f8392e05318cfeb80a0708d2ef0c4147c910b858 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Sep 2023 19:37:50 -0500 Subject: [PATCH 27/49] remove experimental animated qr pause on brightness tip UI --- src/seedsigner/gui/screens/screen.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index ca9da429..59fd24d9 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -752,15 +752,12 @@ class QRDisplayScreen(BaseScreen): while self.keep_running: # convert the self.qr_brightness integer (31-255) into hex triplets hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3 + image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color) # Display the brightness tips toast duration = 10 ** 9 * 1.2 # 1.2 seconds if show_brightness_tips and time.time_ns() - self.tips_start_time.cur_count < duration: - image = self.qr_encoder.part_to_image(self.qr_encoder.cur_part(), 240, 240, border=2, background_color=hex_color) self.add_brightness_tips(image) - else: - # Only advance the QR animation when the brightness tip is not displayed - image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color) with self.renderer.lock: self.renderer.show_image(image) From 03a0e8a549275a95b3161ddf91f2150f6af80a5d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Sep 2023 19:48:43 -0500 Subject: [PATCH 28/49] rename `seed_phrase` argument to `mnemonic` --- src/seedsigner/helpers/qr_encoders.py | 4 ++-- src/seedsigner/views/seed_views.py | 4 ++-- tests/test_encodepsbtqr.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/seedsigner/helpers/qr_encoders.py b/src/seedsigner/helpers/qr_encoders.py index 5dda6978..3c722fcd 100644 --- a/src/seedsigner/helpers/qr_encoders.py +++ b/src/seedsigner/helpers/qr_encoders.py @@ -155,7 +155,7 @@ class BaseXpubQrEncoder(BaseQrEncoder): """ Base Xpub QrEncoder for static and animated formats """ - seed_phrase: list = None + mnemonic: list = None passphrase: str = None derivation: str = None network: str = SettingsConstants.MAINNET @@ -169,7 +169,7 @@ class BaseXpubQrEncoder(BaseQrEncoder): raise Exception('Wordlist Required') version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) - self.seed = Seed(mnemonic=self.seed_phrase, + self.seed = Seed(mnemonic=self.mnemonic, passphrase=self.passphrase, wordlist_language_code=self.wordlist_language_code) self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 183b1a36..0b2cb88a 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -864,7 +864,7 @@ class SeedExportXpubQRDisplayView(View): self.seed = self.controller.get_seed(seed_num) encoder_args = dict( - seed_phrase=self.seed.mnemonic_list, + mnemonic=self.seed.mnemonic_list, passphrase=self.seed.passphrase, derivation=derivation_path, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), @@ -1418,7 +1418,7 @@ class SeedTranscribeSeedQRZoomedInView(View): def run(self): - encoder_args = dict(seed_phrase=self.seed.mnemonic_list, + encoder_args = dict(mnemonic=self.seed.mnemonic_list, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) if self.seedqr_format == QRType.SEED__SEEDQR: e = SeedQrEncoder(**encoder_args) diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index c1765355..35153d7e 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -43,7 +43,7 @@ def test_seedsigner_qr(): def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = StaticXpubQrEncoder(seed_phrase=mnemonic.split(), + e = StaticXpubQrEncoder(mnemonic=mnemonic.split(), passphrase="pass", derivation="m/48h/1h/0h/2h", network=SettingsConstants.TESTNET) @@ -54,7 +54,7 @@ def test_xpub_qr(): def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = SpecterXPubQrEncoder(seed_phrase=mnemonic.split(" "), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + e = SpecterXPubQrEncoder(mnemonic=mnemonic.split(" "), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" @@ -67,7 +67,7 @@ def test_ur_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" e = UrXpubQrEncoder( - seed_phrase=mnemonic.split(), + mnemonic=mnemonic.split(), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", From 98e9ca8fd7e75edc87335396cf2f238d6abc47fc Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 3 Mar 2024 09:08:58 -0600 Subject: [PATCH 29/49] Reverting encode_qr.py file move and name change --- src/seedsigner/gui/screens/screen.py | 2 +- .../{helpers/qr_encoders.py => models/encode_qr.py} | 8 +------- src/seedsigner/views/psbt_views.py | 2 +- src/seedsigner/views/seed_views.py | 2 +- src/seedsigner/views/tools_views.py | 2 +- tests/test_encodepsbtqr.py | 2 +- tests/test_seedqr.py | 2 +- 7 files changed, 7 insertions(+), 13 deletions(-) rename src/seedsigner/{helpers/qr_encoders.py => models/encode_qr.py} (97%) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 59fd24d9..a4b3a742 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -10,7 +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.helpers.qr_encoders import BaseQrEncoder +from seedsigner.models.encode_qr import BaseQrEncoder from seedsigner.models.settings import SettingsConstants from seedsigner.models.threads import BaseThread, ThreadsafeCounter diff --git a/src/seedsigner/helpers/qr_encoders.py b/src/seedsigner/models/encode_qr.py similarity index 97% rename from src/seedsigner/helpers/qr_encoders.py rename to src/seedsigner/models/encode_qr.py index 3c722fcd..159d2be5 100644 --- a/src/seedsigner/helpers/qr_encoders.py +++ b/src/seedsigner/models/encode_qr.py @@ -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,7 +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.models.qr_type import QRType from seedsigner.models.seed import Seed from seedsigner.models.settings import SettingsConstants @@ -26,7 +25,6 @@ class BaseQrEncoder: def __post_init__(self): - print("BaseQrEncoder.__post_init__") self.qr = QR() @@ -162,7 +160,6 @@ class BaseXpubQrEncoder(BaseQrEncoder): wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH def prep_xpub(self): - print("BaseXpubQrEncoder.prep_xpub") self.wordlist = Seed.get_wordlist(self.wordlist_language_code) if self.wordlist == None: @@ -292,7 +289,6 @@ class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder): class BaseFountainQrEncoder(BaseQrEncoder): def __post_init__(self): super().__post_init__() - print("BaseFountainQrEncoder __post_init__") self.ur2_encode: UREncoder = None @@ -338,8 +334,6 @@ class BaseFountainQrEncoder(BaseQrEncoder): class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder): def __post_init__(self): super().__post_init__() - print("UrXpubQrEncoder __post_init__") - self.prep_xpub() def derivation_to_keypath(path: str) -> list: diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index fdb94473..69e12073 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -4,7 +4,7 @@ from embit.networks import NETWORKS from seedsigner.controller import Controller from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants -from seedsigner.helpers.qr_encoders import UrPsbtQrEncoder +from seedsigner.models.encode_qr import UrPsbtQrEncoder from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.settings import SettingsConstants from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 0b2cb88a..0df0889e 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -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.helpers.qr_encoders import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder +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 diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 47b71fa1..162c874a 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -14,7 +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.helpers.qr_encoders import GenericStaticQrEncoder +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 diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 35153d7e..8a8ec368 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -1,4 +1,4 @@ -from seedsigner.helpers.qr_encoders import CompactSeedQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder +from seedsigner.models.encode_qr import CompactSeedQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder from embit import psbt from binascii import a2b_base64 diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 472af661..826cd941 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -2,7 +2,7 @@ import os from embit import bip39 from seedsigner.helpers.qr import QR from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus -from seedsigner.helpers.qr_encoders import SeedQrEncoder, CompactSeedQrEncoder +from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder from seedsigner.models.qr_type import QRType From ea106532b99afefabe34f18ae2024a02cf9ef7bd Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 3 Mar 2024 14:37:33 -0600 Subject: [PATCH 30/49] minor cleanup --- src/seedsigner/models/encode_qr.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 159d2be5..c368c05e 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -54,10 +54,6 @@ class BaseQrEncoder: def part_to_image(self, part, width, height, border: int = 3, background_color: str = "ffffff"): - # 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) return self.qr.qrimage_io(part, width, height, border, background_color=background_color) From 72a27294e580f891e738be2a05cecb9b63cada82 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Sep 2023 21:18:30 -0500 Subject: [PATCH 31/49] live preview green / gray dot decoder indicator --- src/seedsigner/gui/screens/scan_screens.py | 77 ++++++++++++++-- .../helpers/ur2/fountain_decoder.py | 88 ++++++++++++++++--- src/seedsigner/helpers/ur2/ur_decoder.py | 4 +- src/seedsigner/models/decode_qr.py | 15 ++-- 4 files changed, 161 insertions(+), 23 deletions(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 13d3dcf7..a20a5fbb 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -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" @@ -99,7 +110,7 @@ class ScanScreen(BaseScreen): 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" + scan_text = f"{self.decoder.get_percent_complete()}% | {self.decoder.get_percent_complete(weight_mixed_frames=True)}%" if show_framerate: scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}" else: @@ -141,6 +152,47 @@ class ScanScreen(BaseScreen): fill=GUIConstants.BODY_FONT_COLOR, font=instructions_font, anchor="ms") + + # if self.last_frame_decoded_count != self.frames_decoded_counter.cur_count: + # At least one new frame was processed since last screen render; update onscreen status + self.last_frame_decoded_count = self.frames_decoded_counter.cur_count + status_mapping = { + ScanScreen.FRAME__ADDED_PART: "#00ff00", + ScanScreen.FRAME__REPEATED_PART: "#cccccc", + ScanScreen.FRAME__MISS: None, + } + status_color = status_mapping.get(self.frame_decode_status.cur_count) + if status_color: + # Good! Most recent frame successfully decoded. + # Draw an onscreen indication. + # status_block_size = 10 + # draw.rectangle( + # # Lower right + # ( + # (self.renderer.canvas_width - GUIConstants.EDGE_PADDING - status_block_size, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - status_block_size), + # (self.renderer.canvas_width - GUIConstants.EDGE_PADDING, self.renderer.canvas_height - GUIConstants.EDGE_PADDING), + # ), + # # Upper left + # # ( + # # (GUIConstants.EDGE_PADDING, GUIConstants.EDGE_PADDING), + # # (GUIConstants.EDGE_PADDING + status_block_size, GUIConstants.EDGE_PADDING + status_block_size), + # # ), + # # Upper right + # # ( + # # (self.renderer.canvas_width - GUIConstants.EDGE_PADDING - status_block_size, GUIConstants.EDGE_PADDING), + # # (self.renderer.canvas_width - GUIConstants.EDGE_PADDING, GUIConstants.EDGE_PADDING + status_block_size), + # # ), + # fill=status_color, + # ) + indicator_size = 10 + draw.ellipse( + ( + (self.renderer.canvas_width - GUIConstants.EDGE_PADDING - indicator_size, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - indicator_size), + (self.renderer.canvas_width - GUIConstants.EDGE_PADDING, self.renderer.canvas_height - GUIConstants.EDGE_PADDING) + ), + fill=status_color, + ) + self.renderer.show_image(frame, show_direct=True) # print(f" {cur_fps:0.2f} | {self.decoder_fps}") @@ -169,6 +221,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() diff --git a/src/seedsigner/helpers/ur2/fountain_decoder.py b/src/seedsigner/helpers/ur2/fountain_decoder.py index 69eb64e3..c23c62e5 100644 --- a/src/seedsigner/helpers/ur2/fountain_decoder.py +++ b/src/seedsigner/helpers/ur2/fountain_decoder.py @@ -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,50 @@ class FountainDecoder: def result_error(self): return self.result - def estimated_percent_complete(self): + + def estimated_percent_complete(self, weight_mixed_frames: bool = False): 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: + # 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). + 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 + 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.80) + + 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 +130,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 +141,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 +160,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 +168,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 +190,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`... @@ -268,11 +318,27 @@ class FountainDecoder: 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) + res = self.result_description() + # print(f"{self.estimated_percent_complete()*100.0:3.1f}%" + 'parts: {}, received: {}, mixed: {}, queued: {}, result: {}'.format(parts, received, mixed_s, queued, res)) + 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}") + # print(f"{original_metric*100.0:5.1f}% | {guesstimate*100.0:5.1f}% | {mixed_s}") + + except Exception as e: + import traceback + traceback.print_exc() diff --git a/src/seedsigner/helpers/ur2/ur_decoder.py b/src/seedsigner/helpers/ur2/ur_decoder.py index c0baf21f..bb282b09 100644 --- a/src/seedsigner/helpers/ur2/ur_decoder.py +++ b/src/seedsigner/helpers/ur2/ur_decoder.py @@ -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 diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index a020c312..400f7c05 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -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: @@ -305,7 +308,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 @@ -318,6 +321,8 @@ class DecodeQR: for barcode in barcodes: # Only pull and return the first barcode return barcode.data + + print("No QR data") @staticmethod From c1be23ff6356e0860d43f43c0902802aaf904b3f Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 17 Mar 2024 16:47:09 -0500 Subject: [PATCH 32/49] implements progress bar --- src/seedsigner/controller.py | 10 ++ src/seedsigner/gui/components.py | 2 + src/seedsigner/gui/screens/scan_screens.py | 136 ++++++++++++------ src/seedsigner/hardware/camera.py | 2 +- .../helpers/ur2/fountain_decoder.py | 19 ++- src/seedsigner/models/decode_qr.py | 3 +- src/seedsigner/views/scan_views.py | 4 + 7 files changed, 121 insertions(+), 55 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 206ad2c0..4173e89f 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -398,6 +398,16 @@ 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() + print("reset_screensaver_timeout") def activate_toast(self, toast_manager_thread: BaseToastOverlayManagerThread): diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index e505635a..2046f4e1 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -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" diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index a20a5fbb..e5c07c5d 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -96,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 = True show_framerate = False # enable for debugging / testing while self.keep_running: frame = self.camera.read_video_stream(as_image=True) @@ -109,16 +112,23 @@ 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 = f"{self.decoder.get_percent_complete()}% | {self.decoder.get_percent_complete(weight_mixed_frames=True)}%" - if show_framerate: - scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}" - else: + print(f"{cur_fps=}") + + 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( @@ -126,14 +136,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 @@ -152,50 +161,83 @@ class ScanScreen(BaseScreen): fill=GUIConstants.BODY_FONT_COLOR, font=instructions_font, anchor="ms") - - # if self.last_frame_decoded_count != self.frames_decoded_counter.cur_count: - # At least one new frame was processed since last screen render; update onscreen status - self.last_frame_decoded_count = self.frames_decoded_counter.cur_count - status_mapping = { - ScanScreen.FRAME__ADDED_PART: "#00ff00", - ScanScreen.FRAME__REPEATED_PART: "#cccccc", - ScanScreen.FRAME__MISS: None, - } - status_color = status_mapping.get(self.frame_decode_status.cur_count) - if status_color: - # Good! Most recent frame successfully decoded. - # Draw an onscreen indication. - # status_block_size = 10 - # draw.rectangle( - # # Lower right - # ( - # (self.renderer.canvas_width - GUIConstants.EDGE_PADDING - status_block_size, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - status_block_size), - # (self.renderer.canvas_width - GUIConstants.EDGE_PADDING, self.renderer.canvas_height - GUIConstants.EDGE_PADDING), - # ), - # # Upper left - # # ( - # # (GUIConstants.EDGE_PADDING, GUIConstants.EDGE_PADDING), - # # (GUIConstants.EDGE_PADDING + status_block_size, GUIConstants.EDGE_PADDING + status_block_size), - # # ), - # # Upper right - # # ( - # # (self.renderer.canvas_width - GUIConstants.EDGE_PADDING - status_block_size, GUIConstants.EDGE_PADDING), - # # (self.renderer.canvas_width - GUIConstants.EDGE_PADDING, GUIConstants.EDGE_PADDING + status_block_size), - # # ), - # fill=status_color, - # ) - indicator_size = 10 - draw.ellipse( + + 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( ( - (self.renderer.canvas_width - GUIConstants.EDGE_PADDING - indicator_size, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - indicator_size), - (self.renderer.canvas_width - GUIConstants.EDGE_PADDING, self.renderer.canvas_height - GUIConstants.EDGE_PADDING) + (0, 0), + (rectangle.width, rectangle.height) ), - fill=status_color, + 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 diff --git a/src/seedsigner/hardware/camera.py b/src/seedsigner/hardware/camera.py index 32dc182d..59ff4101 100644 --- a/src/seedsigner/hardware/camera.py +++ b/src/seedsigner/hardware/camera.py @@ -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 diff --git a/src/seedsigner/helpers/ur2/fountain_decoder.py b/src/seedsigner/helpers/ur2/fountain_decoder.py index c23c62e5..b237ef86 100644 --- a/src/seedsigner/helpers/ur2/fountain_decoder.py +++ b/src/seedsigner/helpers/ur2/fountain_decoder.py @@ -73,6 +73,13 @@ class FountainDecoder: 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). + TODO: try just using the largest fraction of the XOR total + """ if self.is_complete(): return 1 if self.expected_part_indexes == None: @@ -83,11 +90,6 @@ class FountainDecoder: estimated_input_parts = self.expected_part_count() * 1.75 return min(0.99, self.processed_parts_count / estimated_input_parts) else: - # 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). parts = self.expected_part_count() if self.expected_part_indexes != None else 'None' mixed = [] mixed_index_scoring = {} @@ -101,15 +103,20 @@ class FountainDecoder: for index in indexes: if index not in mixed_index_scoring: mixed_index_scoring[index] = 0.0 + + # metric 1: sum up partial scores mixed_index_scoring[index] += score + # metric 2: just keep the max + # mixed_index_scoring[index] = max(score, mixed_index_scoring[index]) + 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.80) + mixed_score += min(score, 0.75) num_complete = len(self.received_part_indexes) weighted_estimate = (num_complete + mixed_score) / float(parts) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 400f7c05..96412ccc 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -264,6 +264,7 @@ class DecodeQR: QRType.PSBT__BASE43, ] + @property def is_seed(self): return self.qr_type in [ @@ -308,7 +309,7 @@ class DecodeQR: @staticmethod - def extract_qr_data(image, is_binary:bool = False) -> [str|None]: + def extract_qr_data(image, is_binary:bool = False) -> str | None: if image is None: return None diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index 17f71333..7bac9a45 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -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: From f15fdfd8fd1c1c967d0bc55f0feba861ca2971d6 Mon Sep 17 00:00:00 2001 From: Marcel Hernandez Date: Sun, 31 Mar 2024 00:53:22 +0100 Subject: [PATCH 33/49] turn on P2TR scripts by default --- src/seedsigner/models/settings_definition.py | 2 +- tests/test_controller.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 76534cec..c329d82e 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -420,7 +420,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, diff --git a/tests/test_controller.py b/tests/test_controller.py index 9eacc792..375ae9ff 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -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 From 34a976ef12271dcdec4eb336ee53f96d4357639d Mon Sep 17 00:00:00 2001 From: Aneesh Karve Date: Sun, 31 Mar 2024 17:57:35 -0700 Subject: [PATCH 34/49] README note: don't mount .img before signature verification --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1f6a0a10..8abd4045 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,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: From fa953876fa41a3047c7d2543521678618e5a014d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 3 Apr 2024 11:35:16 -0500 Subject: [PATCH 35/49] cleanup, commenting out debugging --- src/seedsigner/controller.py | 1 - src/seedsigner/gui/screens/scan_screens.py | 5 ++--- .../helpers/ur2/fountain_decoder.py | 22 +++++-------------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 4173e89f..f0a95287 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -407,7 +407,6 @@ class Controller(Singleton): """ from seedsigner.hardware.buttons import HardwareButtons HardwareButtons.get_instance().update_last_input_time() - print("reset_screensaver_timeout") def activate_toast(self, toast_manager_thread: BaseToastOverlayManagerThread): diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index e5c07c5d..e1af8e15 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -104,7 +104,7 @@ class ScanScreen(BaseScreen): start_time = time.time() num_frames = 0 - debug = True + debug = False show_framerate = False # enable for debugging / testing while self.keep_running: frame = self.camera.read_video_stream(as_image=True) @@ -112,8 +112,7 @@ class ScanScreen(BaseScreen): num_frames += 1 cur_time = time.time() cur_fps = num_frames / (cur_time - start_time) - print(f"{cur_fps=}") - + scan_text = None progress_percentage = self.decoder.get_percent_complete() if progress_percentage == 0: diff --git a/src/seedsigner/helpers/ur2/fountain_decoder.py b/src/seedsigner/helpers/ur2/fountain_decoder.py index b237ef86..9e8dcdf3 100644 --- a/src/seedsigner/helpers/ur2/fountain_decoder.py +++ b/src/seedsigner/helpers/ur2/fountain_decoder.py @@ -78,7 +78,6 @@ class FountainDecoder: * 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). - TODO: try just using the largest fraction of the XOR total """ if self.is_complete(): return 1 @@ -104,12 +103,9 @@ class FountainDecoder: if index not in mixed_index_scoring: mixed_index_scoring[index] = 0.0 - # metric 1: sum up partial scores + # sum up partial scores mixed_index_scoring[index] += score - # metric 2: just keep the max - # mixed_index_scoring[index] = max(score, mixed_index_scoring[index]) - 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 @@ -117,11 +113,11 @@ class FountainDecoder: # 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 @@ -148,11 +144,11 @@ class FountainDecoder: self.processed_parts_count += 1 # self.print_part_end() - self.print_state() + # 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") + # print("No new data") return False return True @@ -176,7 +172,7 @@ class FountainDecoder: else: self.process_mixed_part(part) - print(f"Queue processing: {int((time.time() - start)*1000.0)}ms") + # print(f"Queue processing: {int((time.time() - start)*1000.0)}ms") # self.print_state() def reduce_mixed_by(self, p): @@ -323,9 +319,6 @@ 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 = [] @@ -341,10 +334,7 @@ class FountainDecoder: mixed_s = "[{}]".format(', '.join(mixed)) queued = len(self.queued_parts) - res = self.result_description() - # print(f"{self.estimated_percent_complete()*100.0:3.1f}%" + 'parts: {}, received: {}, mixed: {}, queued: {}, result: {}'.format(parts, received, mixed_s, queued, res)) 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}") - # print(f"{original_metric*100.0:5.1f}% | {guesstimate*100.0:5.1f}% | {mixed_s}") except Exception as e: import traceback From bb565c8c7d544c7e229e957ae78b13b3a1c65230 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 3 Apr 2024 12:48:48 -0500 Subject: [PATCH 36/49] cleanup --- src/seedsigner/models/decode_qr.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 96412ccc..da5f2526 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -322,8 +322,6 @@ class DecodeQR: for barcode in barcodes: # Only pull and return the first barcode return barcode.data - - print("No QR data") @staticmethod @@ -343,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 @@ -368,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 @@ -398,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 @@ -414,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: From da459eedacedcc6f1ead7ad61417e60c7368ad6b Mon Sep 17 00:00:00 2001 From: Daniel Bast <2790401+dbast@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:34:22 +0200 Subject: [PATCH 37/49] Enable daily builds with badge and add os-ref param for manual builds This updates the existing build action to: * add an os-ref input parameter to be able to do manual workflow_dispatch builds where any seedsigner branch can be combined with any seedsigner-os ref (tag, branch, sha1). (default = the default branch of both repos) * produce unique (git describe based)image file names that have the seedsigner and seedsigner-os version encoded, e.g. seedsigner_os.os0.6.0-61-g9fafebe_sw0.7.0-40-g4b7c895.pi0.img to know exactly what version an image is based on. * simplify the cache action step: restore + save is automatically done by the plain action and its included post-action step. Some testing shows that the cache action reduces the build time to 50% (~30mins). * enable daily builds = daily PoW that everything works, sources exist, and builds fine together. And it also enables easily testing of merged PRs of the last days for everybody without locally building anything or waiting for a manual workflow_dispatch run. (this also keeps the cache warm, so succeeding triggered builds are a lot faster). * lower the retention-days to 7 (default 90 days), to not consume to much artifact space with the daily builds enabled. For more details see inline comments. Succeeding PRs can also enable builds for every merge to the dev/main branch or for triggering builds when a tag is added. # Conflicts: # README.md --- .github/workflows/build.yml | 70 +++++++++++++++++++++++-------------- README.md | 1 + 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dd0fec7..af2e1e2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,12 +1,22 @@ name: Build on: + schedule: + # Build daily at 5:30 UTC + - cron: '30 5 * * *' workflow_dispatch: + inputs: + os-ref: + description: The seedsigner-os ref (tag/branch/sha1) to use + default: main + required: true 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 +26,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 +70,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 +86,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 +93,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 +110,7 @@ jobs: name: seedsigner_os_images path: "seedsigner-os/images/*.img" if-no-files-found: error + retention-days: 7 sha256sum: name: calculate sha256sum @@ -128,4 +143,5 @@ jobs: with: name: seedsigner_os_images path: "images/*.sha256" - if-no-files-found: error \ No newline at end of file + if-no-files-found: error + retention-days: 7 diff --git a/README.md b/README.md index 407d8b3b..098a8aed 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ # 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. From 783ed5ed52775ad76534c66658e693166fedb778 Mon Sep 17 00:00:00 2001 From: Daniel Bast <2790401+dbast@users.noreply.github.com> Date: Fri, 22 Sep 2023 12:02:21 +0200 Subject: [PATCH 38/49] Test build.yml workflow file when PRs change it --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af2e1e2c..b464ad9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,10 @@ name: Build on: + pull_request: + # Build on changes to this workflow files in PRs to test proposed changes + paths: + - '.github/workflows/build.yml' schedule: # Build daily at 5:30 UTC - cron: '30 5 * * *' From 36ddcf209c875cc097b769179502f50fc42e44e6 Mon Sep 17 00:00:00 2001 From: Daniel Bast <2790401+dbast@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:48:09 +0200 Subject: [PATCH 39/49] Switch to branch trigger builds (instead of cron), increase rentention --- .github/workflows/build.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b464ad9e..a0649dc7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,9 +5,10 @@ on: # Build on changes to this workflow files in PRs to test proposed changes paths: - '.github/workflows/build.yml' - schedule: - # Build daily at 5:30 UTC - - cron: '30 5 * * *' + push: + branches: + - main + - dev workflow_dispatch: inputs: os-ref: @@ -15,6 +16,9 @@ on: 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 @@ -114,7 +118,8 @@ jobs: name: seedsigner_os_images path: "seedsigner-os/images/*.img" if-no-files-found: error - retention-days: 7 + # maximum 90 days retention + retention-days: 90 sha256sum: name: calculate sha256sum @@ -148,4 +153,5 @@ jobs: name: seedsigner_os_images path: "images/*.sha256" if-no-files-found: error - retention-days: 7 + # maximum 90 days retention + retention-days: 90 From 2005fd62b11c36e3726187fb5ce142ac60e375d3 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Thu, 14 Sep 2023 23:04:02 -0400 Subject: [PATCH 40/49] Update README to point to SeedSigner OS build instructions and differentiate from the older Raspberry Pi OS build instructions. --- README.md | 16 +++++++++++----- ....md => raspberry_pi_os_build_instructions.md} | 4 +++- 2 files changed, 14 insertions(+), 6 deletions(-) rename docs/{manual_installation.md => raspberry_pi_os_build_instructions.md} (97%) diff --git a/README.md b/README.md index 407d8b3b..5ab95d80 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ * [Verifying the Software](#verifying-the-software) * [Enclosure Designs](#enclosure-designs) * [SeedQR Printable Templates](#seedqr-printable-templates) -* [Manual Installation Instructions](#manual-installation-instructions) +* [Developer Build Instructions](#developer-build-instructions) +* [Build from Source](#build-from-source) --------------- @@ -82,9 +83,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 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. -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). +As of release 0.7.0 the image released are reproducible. This means you and others can verify the released images are byte for byte that same when built from source. If you have the ability to read and understand source code; then 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 @@ -326,5 +329,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) +# Developer Build Instructions +Raspberry Pi OS is commonly used for development. See the [Raspberry Pi OS Build Instructions](docs/raspberry_pi_os_build_instructions.md) + +# Build from Source +See the [SeedSigner OS repo](https://github.com/SeedSigner/seedsigner-os/) for instructions. \ No newline at end of file diff --git a/docs/manual_installation.md b/docs/raspberry_pi_os_build_instructions.md similarity index 97% rename from docs/manual_installation.md rename to docs/raspberry_pi_os_build_instructions.md index 17dbc975..eba85cc1 100644 --- a/docs/manual_installation.md +++ b/docs/raspberry_pi_os_build_instructions.md @@ -1,4 +1,6 @@ -# Manual Installation Instructions +# Raspberry Pi OS Build Instructions + +The released version of [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/) does not use Raspberry Pi OS since version 0.6.0. These build instructions (previously referred to as "Manual Installation Instructions") are most commonly used by projects contributors looking to do rapid development cycles and testing to app code. If this is what you'd like to do, then 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: From 394eb46a8f42c5bf283ef77b2ae67ee3dea38af8 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Fri, 15 Sep 2023 00:21:03 -0400 Subject: [PATCH 41/49] Fix typos --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5ab95d80..ab817f52 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,9 @@ 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 people 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. -As of release 0.7.0 the image released are reproducible. This means you and others can verify the released images are byte for byte that same when built from source. If you have the ability to read and understand source code; then you can contribute to this project by building from source and sharing the hash of the final images. +As of release 0.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. If you have the ability to read and understand source code; then 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). @@ -333,4 +333,4 @@ Letter templates(8.5in * 11in): Raspberry Pi OS is commonly used for development. See the [Raspberry Pi OS Build Instructions](docs/raspberry_pi_os_build_instructions.md) # Build from Source -See the [SeedSigner OS repo](https://github.com/SeedSigner/seedsigner-os/) for instructions. \ No newline at end of file +See the [SeedSigner OS repo](https://github.com/SeedSigner/seedsigner-os/) for instructions. From 1fa3f806789dd57c3d5b7716fa7474d4bf12ad42 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Fri, 15 Sep 2023 00:22:36 -0400 Subject: [PATCH 42/49] fix type --- docs/raspberry_pi_os_build_instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/raspberry_pi_os_build_instructions.md b/docs/raspberry_pi_os_build_instructions.md index eba85cc1..a0723c73 100644 --- a/docs/raspberry_pi_os_build_instructions.md +++ b/docs/raspberry_pi_os_build_instructions.md @@ -1,6 +1,6 @@ # Raspberry Pi OS Build Instructions -The released version of [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/) does not use Raspberry Pi OS since version 0.6.0. These build instructions (previously referred to as "Manual Installation Instructions") are most commonly used by projects contributors looking to do rapid development cycles and testing to app code. If this is what you'd like to do, then continue reading. +The released version of [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/) does not use Raspberry Pi OS since version 0.6.0. These build instructions (previously referred to as "Manual Installation Instructions") are most commonly used by project contributors looking to do rapid development cycles and testing to app code. If this is what you'd like to do, then 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: From 367d037406ee9bb965fb106d55d5c24bdcd20ffa Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Fri, 15 Sep 2023 00:24:43 -0400 Subject: [PATCH 43/49] fix more typos --- docs/raspberry_pi_os_build_instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/raspberry_pi_os_build_instructions.md b/docs/raspberry_pi_os_build_instructions.md index a0723c73..59b3ee53 100644 --- a/docs/raspberry_pi_os_build_instructions.md +++ b/docs/raspberry_pi_os_build_instructions.md @@ -1,6 +1,6 @@ # Raspberry Pi OS Build Instructions -The released version of [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/) does not use Raspberry Pi OS since version 0.6.0. These build instructions (previously referred to as "Manual Installation Instructions") are most commonly used by project contributors looking to do rapid development cycles and testing to app code. If this is what you'd like to do, then continue reading. +The released version of [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/) does not use Raspberry Pi OS since version 0.6.0. These build instructions (previously referred to as "Manual Installation Instructions") are most commonly used by project contributors looking to do rapid development cycles. If this is what you'd like to do, then 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: From 3ca3174b05d8ac4085e8665223d9bab2c7eaf152 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Fri, 12 Apr 2024 08:42:29 -0400 Subject: [PATCH 44/49] Update README.md minor edits from Keith --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ab817f52..54958c76 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ * [Verifying the Software](#verifying-the-software) * [Enclosure Designs](#enclosure-designs) * [SeedQR Printable Templates](#seedqr-printable-templates) -* [Developer Build Instructions](#developer-build-instructions) * [Build from Source](#build-from-source) +* [Developer Local Build Instructions](#developer-build-instructions) --------------- @@ -85,7 +85,7 @@ Notes: ## 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 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. -As of release 0.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. If you have the ability to read and understand source code; then you can contribute to this project by building from source and sharing the hash of the final images. +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). @@ -329,8 +329,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) --------------- -# Developer Build Instructions -Raspberry Pi OS is commonly used for development. See the [Raspberry Pi OS Build Instructions](docs/raspberry_pi_os_build_instructions.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) From 8cbcfb0c77e40ac6937d88d560ef3998d5fffe9e Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Fri, 12 Apr 2024 09:01:51 -0400 Subject: [PATCH 45/49] Update README.md correct developer local build instructions link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54958c76..9346ea12 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ * [Enclosure Designs](#enclosure-designs) * [SeedQR Printable Templates](#seedqr-printable-templates) * [Build from Source](#build-from-source) -* [Developer Local Build Instructions](#developer-build-instructions) +* [Developer Local Build Instructions](#developer-local-build-instructions) --------------- From 1ff9ac0302cd0b2d535da8d6b04d50d32774b04b Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Fri, 12 Apr 2024 09:04:45 -0400 Subject: [PATCH 46/49] Update raspberry_pi_os_build_instructions.md minor edits from Keith --- docs/raspberry_pi_os_build_instructions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/raspberry_pi_os_build_instructions.md b/docs/raspberry_pi_os_build_instructions.md index 59b3ee53..01c1d255 100644 --- a/docs/raspberry_pi_os_build_instructions.md +++ b/docs/raspberry_pi_os_build_instructions.md @@ -1,6 +1,6 @@ -# Raspberry Pi OS Build Instructions +# Raspberry Pi OS Local Dev Build Instructions -The released version of [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/) does not use Raspberry Pi OS since version 0.6.0. These build instructions (previously referred to as "Manual Installation Instructions") are most commonly used by project contributors looking to do rapid development cycles. If this is what you'd like to do, then continue reading. +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: From fd413f3a05b24a0686de29bb46ca7d434e214932 Mon Sep 17 00:00:00 2001 From: Daniel Bast <2790401+dbast@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:03:44 +0200 Subject: [PATCH 47/49] Remove unused dependencies from requirements.txt This removes numpy and six from requirements.txt as there are no left references in the code. --- docs/raspberry_pi_os_build_instructions.md | 2 +- requirements.txt | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/raspberry_pi_os_build_instructions.md b/docs/raspberry_pi_os_build_instructions.md index 01c1d255..fdfb894e 100644 --- a/docs/raspberry_pi_os_build_instructions.md +++ b/docs/raspberry_pi_os_build_instructions.md @@ -111,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 ``` diff --git a/requirements.txt b/requirements.txt index 65c471ef..f86231b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 9e1d55cc2d971a7f7f0194aff62fc79bde1b8bac Mon Sep 17 00:00:00 2001 From: Daniel Bast <2790401+dbast@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:31:48 +0200 Subject: [PATCH 48/49] Move numpy to requirements-raspi.txt --- requirements-raspi.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements-raspi.txt b/requirements-raspi.txt index 8053eeab..d86e93d2 100644 --- a/requirements-raspi.txt +++ b/requirements-raspi.txt @@ -1,3 +1,5 @@ picamera==1.13 +# numpy = transitive picamera dependency +numpy==1.25.2 RPi.GPIO==0.7.0 spidev==3.5 \ No newline at end of file From 0dcbd63a418901916aa6511ab8728e278776b64e Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Mon, 29 Apr 2024 10:50:59 -0400 Subject: [PATCH 49/49] remove DireWarning just added in PR 533 --- src/seedsigner/views/psbt_views.py | 13 ------------- tests/test_flows_psbt.py | 2 -- 2 files changed, 15 deletions(-) diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 8f478a26..18e40312 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -330,11 +330,9 @@ class PSBTChangeDetailsView(View): if is_change_derivation_path: title = "Your Change" self.VERIFY_MULTISIG = "Verify Multisig Change" - unverified_warning_text = "Can't Verify that Change Outputs Belong to your Wallet" else: title = "Self-Transfer" self.VERIFY_MULTISIG = "Verify Multisig Addr" - unverified_warning_text = "Can't Verify that Self-Transfer Outputs Belong to your Wallet" # if psbt_parser.num_change_outputs > 1: # title += f" (#{self.change_address_num + 1})" @@ -416,17 +414,6 @@ class PSBTChangeDetailsView(View): return Destination(BackStackView) elif button_data[selected_menu_num] == self.NEXT or button_data[selected_menu_num] == self.SKIP_VERIFICATION: - if button_data[selected_menu_num] == self.SKIP_VERIFICATION: - selected_menu_num = self.run_screen( - DireWarningScreen, - title="Security Warning", - status_icon_name=SeedSignerIconConstants.WARNING, - status_headline="Potential Loss of Funds", - text=unverified_warning_text, - ) - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) if self.change_address_num < psbt_parser.num_change_outputs - 1: return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1}) diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py index fb83e626..3f487380 100644 --- a/tests/test_flows_psbt.py +++ b/tests/test_flows_psbt.py @@ -73,7 +73,6 @@ class TestPSBTFlows(FlowTest): FlowStep(psbt_views.PSBTMathView), FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0), FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.SKIP_VERIFICATION), - FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=0), 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,6 @@ class TestPSBTFlows(FlowTest): FlowStep(psbt_views.PSBTMathView), FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0), FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.SKIP_VERIFICATION), - FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=0), FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT), FlowStep(psbt_views.PSBTSignedQRDisplayView), FlowStep(MainMenuView),