diff --git a/tests/test_controller.py b/tests/test_controller.py index 96757eff..ef3e7ea3 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -2,6 +2,7 @@ import configparser import pytest from mock import MagicMock from seedsigner.controller import Controller +from seedsigner.models.settings import Settings @@ -17,6 +18,13 @@ def test_singleton_get_instance_without_configure_fails(): def test_singleton_get_instance_preserves_state(): """ Changes to the Controller singleton should be preserved across calls to get_instance() """ + + # Must reset Singleton instances; pytest cannot properly isolate Singletons for us + # automatically. + # TODO: Cleaner solution here would be nice. + Settings._instance = None + Controller._instance = None + settings = """ [system] debug = False @@ -25,6 +33,53 @@ def test_singleton_get_instance_preserves_state(): [display] text_color = ORANGE + qr_background_color = FFFFFF + camera_rotation = 0 + + [wallet] + network = main + software = Prompt + qr_density = 2 + custom_derivation = m/0/0 + compact_seedqr_enabled = False + """ + config = configparser.ConfigParser() + config.read_string(settings) + + # Initialize the instance and verify that it read the config settings + Controller.configure_instance(config, disable_hardware=True) + controller = Controller.get_instance() + assert controller.color == "ORANGE" + + # Change a value in the instance... + controller.color = "purple" + + # ...get a new copy of the instance and confirm change + controller = Controller.get_instance() + assert controller.color == "purple" + + +def test_missing_settings_get_defaults(): + """ Should gracefully handle any missing fields from `settings.ini` """ + # TODO: This is not complete; currently only handles missing compact_seedqr_enabled. + + # Must reset Singleton instances; pytest cannot properly isolate Singletons for us + # automatically. + # TODO: Cleaner solution here would be nice. + Settings._instance = None + Controller._instance = None + + # Intentionally omit `compact_seedqr_enabled` from settings: + settings = """ + [system] + debug = False + default_language = en + persistent_settings = False + + [display] + text_color = ORANGE + qr_background_color = FFFFFF + camera_rotation = 0 [wallet] network = main @@ -35,14 +90,9 @@ def test_singleton_get_instance_preserves_state(): config = configparser.ConfigParser() config.read_string(settings) - # Initialize the instance and verify that it read the config settings - Controller.configure_instance(config) - controller = Controller.get_instance() - assert controller.color == "ORANGE" + # Controller should parse the settings fine, even though a field is missing + Controller.configure_instance(config, disable_hardware=True) - # Change a value in the instance... - controller.color = "purple" - - # ...get a new copy of the instance and confirm change + # Controller should still have a default value controller = Controller.get_instance() - assert controller.color == "purple" + assert controller.settings.compact_seedqr_enabled is False diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index 270ae0f7..7d673fd2 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -41,7 +41,7 @@ def test_ur_qr_decode_multisig(): d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: - d.addString(i) + d.add_data(i) assert d.qrType() == QRType.PSBTUR2 #complete should be true @@ -63,7 +63,7 @@ def test_base64_single_frame_singlsig(): d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: - d.addString(i) + d.add_data(i) assert d.qrType() == QRType.PSBTBASE64 #complete should be true @@ -108,7 +108,7 @@ def test_ur2_sparrow_singlesig_to_self(): d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: - d.addString(i) + d.add_data(i) assert d.qrType() == QRType.PSBTUR2 #complete should be true @@ -127,7 +127,7 @@ def test_base64_2_input_p2wsh(): base64_psbt = "cHNidP8BALICAAAAAq1DhxRK+mUH4T6uUNob8bUaZ7MP+44MW4+Y9bOxpjhZAAAAAAD9////aWclWQ+45HKrI07r878E2UrAupT2paT4QurbmtNjYNQBAAAAAP3///8CQEIPAAAAAAAiACCpkDPDhmIzPlkJrjw9A71xjbIUWf3VUB7ooFJhTVm04tjSIQEAAAAAIgAgjQKFDauIXsV5u23LBdYgOwX1FwGGrLiQfWzBtFKZ7dIAAAAATwEENYfPBD5i336AAAACQStJhNVJul7vHKbo83VdmuAW2m0WaXLKDlFANn7dUNoCNbhLMdw4Knz7Q7o6exdL6UFhQegW9nJb0SUStbLEpawUAgjLdzAAAIABAACAAAAAgAIAAIBPAQQ1h88EnbHQAIAAAAI/2Nc7x7iMpJNapTe/OJTV4oifqzQcYY9KV2+PGRjCdQJoww1WnSNqfcxXGyux0q1PqfmzUqgJNqKJCpmqI9t47BQmu4PEMAAAgAEAAIAAAACAAgAAgE8BBDWHzwS6wUg5gAAAAh1Pvr3ZZ+GvcUwJl9OPz2cLXOnTAcBEC7zDtqIOt3IcA1aOofNgUZFu0baQw54SqOcGA7KAvTDOXygfKRilU2OqFHPF2gowAACAAQAAgAAAAIACAACAAAEBK4CWmAAAAAAAIgAgiYAxcG7dnrEiZ4VHFVHOo18XCalvhZYuMqBr9n7HESQBBWlSIQJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8iEDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMchA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHU64iBgJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8hwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMccAgjLdzAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHHHPF2gowAACAAQAAgAAAAIACAACAAAAAAAEAAAAAAQErgJaYAAAAAAAiACAzd60wM9EFnPHSNbsSJfyipL8myVLVP2/vwzotVUSNxQEFaVIhAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiIQKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdCEDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgFTriIGAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiHAIIy3cwAACAAQAAgAAAAIACAACAAAAAAAAAAAAiBgKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdBwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgEcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAABAWlSIQKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/iEDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYhA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrU64iAgKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/hwCCMt3MAAAgAEAAIAAAACAAgAAgAEAAAAAAAAAIgIDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYcc8XaCjAAAIABAACAAAAAgAIAAIABAAAAAAAAACICA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrHCa7g8QwAACAAQAAgAAAAIACAACAAQAAAAAAAAAAAA==" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(base64_psbt) + d.add_data(base64_psbt) assert d.qrType() == QRType.PSBTBASE64 tx = d.getPSBT() @@ -154,7 +154,7 @@ def test_base64_1_input_p2sh_p2wsh(): base64_psbt = "cHNidP8BAHICAAAAAR30J629i3Y/R8woRpLQ9JUa31rKxyM+Ny4NEsme48GWAAAAAAD9////Atw5XQUAAAAAF6kUdSESczdYagEyToVUSXyT8VTNz+OHgJaYAAAAAAAWABTmav7/w4OOcfCiewfjsA7eaujhYAAAAABPAQQ1h88EPmLffoAAAAHdXEj2dn8EYJ+rRdXEYu5laq6lJI5Mp+3t63ckwty05QKrJBNPewhwQaGPYRif6+XaxozFXvTXn7pU24H6fRy1FxQCCMt3MAAAgAEAAIAAAACAAQAAgE8BBDWHzwSdsdAAgAAAAeeOv56oeaaFTrNonMKDHk1C8brbWGFvdlecVue+v0/RAn/g4yI3oYsyen7OOcT7caYl4Mn7nQbyonHcusUR+GhzFCa7g8QwAACAAQAAgAAAAIABAACATwEENYfPBLrBSDmAAAABpzrb4oeEh2NNy/w/fr3osfyZTx7AaGDPAcP+LqeR3bYC5ioqmXPuazp69HwimlvecLylm9BLuyl/VpPXqiVrl20Uc8XaCjAAAIABAACAAAAAgAEAAIAAAQEgAOH1BQAAAAAXqRSv3gkn8731qcPbSDu4TJOlJJZ/PocBBCIAIOeiFBX5x0vX6CacrAUVovrs1DDCcKJS5qptFS3sjpDpAQVpUiECZ+pFYkOTVjB+eG+vQFA3MNjZWiA6DjRcs1Wl36A/zgMhA2Ygckuwjah29wiVRgA6wFx51+6ayrzeCIQ2eE4zfxPtIQOnUlBn22cn2CPCZkMSI6cDaZK2SlLV20rT6pqMoQCJsFOuIgYCZ+pFYkOTVjB+eG+vQFA3MNjZWiA6DjRcs1Wl36A/zgMcc8XaCjAAAIABAACAAAAAgAEAAIAAAAAAAAAAACIGA2Ygckuwjah29wiVRgA6wFx51+6ayrzeCIQ2eE4zfxPtHCa7g8QwAACAAQAAgAAAAIABAACAAAAAAAAAAAAiBgOnUlBn22cn2CPCZkMSI6cDaZK2SlLV20rT6pqMoQCJsBwCCMt3MAAAgAEAAIAAAACAAQAAgAAAAAAAAAAAAAEAIgAgZBwTq05RkpqKv6FV6LQjuM07Qv0/bYfWVc9NUQOFvwQBAWlSIQI32jVSdTgeu7+YZKrWfgOZ2J/LV36c5rBoApTzhrNlDCEC+hEqm3XmRt862AFFeyJ7p1m8A+V7czj6OajUNgCfg4EhA/MUVfxGh4k3Po3LB8CmMRsvIHcGNO0elUgETaITZA3UU64iAgI32jVSdTgeu7+YZKrWfgOZ2J/LV36c5rBoApTzhrNlDBwCCMt3MAAAgAEAAIAAAACAAQAAgAEAAAAAAAAAIgIC+hEqm3XmRt862AFFeyJ7p1m8A+V7czj6OajUNgCfg4EcJruDxDAAAIABAACAAAAAgAEAAIABAAAAAAAAACICA/MUVfxGh4k3Po3LB8CmMRsvIHcGNO0elUgETaITZA3UHHPF2gowAACAAQAAgAAAAIABAACAAQAAAAAAAAAAAA==" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(base64_psbt) + d.add_data(base64_psbt) assert d.qrType() == QRType.PSBTBASE64 tx = d.getPSBT() @@ -208,7 +208,7 @@ def test_ur_legacy(): d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: - if d.addString(i) == DecodeQRStatus.COMPLETE: + if d.add_data(i) == DecodeQRStatus.COMPLETE: break assert d.qrType() == QRType.PSBTURLEGACY @@ -242,7 +242,7 @@ def test_specter_multisig_animated_qr(): d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: - if d.addString(i) == DecodeQRStatus.COMPLETE: + if d.add_data(i) == DecodeQRStatus.COMPLETE: break assert d.qrType() == QRType.PSBTSPECTER @@ -280,7 +280,7 @@ def test_specter_multisig_animated_qr(): d2 = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes2: - if d2.addString(i) == DecodeQRStatus.COMPLETE: + if d2.add_data(i) == DecodeQRStatus.COMPLETE: break assert d2.qrType() == QRType.PSBTSPECTER @@ -313,22 +313,22 @@ def test_mnemonic_qr(): mnemonic = "height demise useless trap grow lion found off key clown transfer enroll" d = DecodeQR(wordlist=bip39.WORDLIST) - assert d.addString(mnemonic) == DecodeQRStatus.COMPLETE + assert d.add_data(mnemonic) == DecodeQRStatus.COMPLETE assert d.getSeedPhrase() == mnemonic.split() bad_mnemonic = "height useless trap grow lion found off key clown transfer enroll" d2 = DecodeQR(wordlist=bip39.WORDLIST) - assert d2.addString(bad_mnemonic) == DecodeQRStatus.INVALID + assert d2.add_data(bad_mnemonic) == DecodeQRStatus.INVALID bad2_mnemonic = "words not in the list of bip39" d3 = DecodeQR(wordlist=bip39.WORDLIST) - assert d3.addString(bad2_mnemonic) == DecodeQRStatus.INVALID + assert d3.add_data(bad2_mnemonic) == DecodeQRStatus.INVALID def test_short_4_letter_mnemonic_qr(): short_nm = "heig demi usel trap grow lion foun off key clow tran enro" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(short_nm) + d.add_data(short_nm) assert d.isComplete() == True assert d.getSeedPhrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"] @@ -352,54 +352,54 @@ def test_bitcoin_address(): main_bech32_address3 = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?junk" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(bad1) + d.add_data(bad1) assert d.qrType() == QRType.INVALID d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(legacy_address1) + d.add_data(legacy_address1) assert d.getAddress() == legacy_address1 assert d.getAddressType() == "P2PKH-main" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(legacy_address2) + d.add_data(legacy_address2) assert d.getAddress() == legacy_address2 assert d.getAddressType() == "P2PKH-main" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(main_bech32_address) + d.add_data(main_bech32_address) assert d.getAddress() == main_bech32_address assert d.getAddressType() == "Bech32-main" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(test_bech32_address) + d.add_data(test_bech32_address) assert d.getAddress() == test_bech32_address assert d.getAddressType() == "Bech32-test" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(main_nested_segwit_address) + d.add_data(main_nested_segwit_address) assert d.getAddress() == main_nested_segwit_address assert d.getAddressType() == "P2SH-main" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(test_nested_segwit_address) + d.add_data(test_nested_segwit_address) assert d.getAddress() == test_nested_segwit_address assert d.getAddressType() == "P2SH-test" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(main_bech32_address2) + d.add_data(main_bech32_address2) assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" assert d.getAddressType() == "Bech32-main" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(main_bech32_address3) + d.add_data(main_bech32_address3) assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" assert d.getAddressType() == "Bech32-main" @@ -408,9 +408,9 @@ def test_seed_qr(): seed = "121802020768124106400009195602431595117715840445" d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(seed) + d.add_data(seed) - assert d.qrType() == QRType.SEEDSSQR + assert d.qrType() == QRType.SEEDQR assert d.getSeedPhrase() == "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split() def test_specter_wallet_json(): @@ -422,9 +422,9 @@ def test_specter_wallet_json(): ] d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(parts[1]) - d.addString(parts[2]) - d.addString(parts[0]) + d.add_data(parts[1]) + d.add_data(parts[2]) + d.add_data(parts[0]) assert d.qrType() == QRType.SPECTERWALLETQR assert d.isComplete() == True @@ -432,10 +432,10 @@ def test_specter_wallet_json(): def test_specter_wallet_json2(): - 1_part = '{"label": "Testnet Single Zone", "blockheight": 2090512, "descriptor": "wpkh([990a73ad/84h/1h/0h]tpubDDHQMDnFdan2GyHBsG32VW9qiygbhVizGRTjiS3H79M49FSvpsvLXqLgp1yC7r43dXVHozWavi2Fc4WHUpZmQYmzoQbit28qJhLjScbAQWU/0/*)#ujr0xunp","devices": [{"type": "seedsigner", "label": "Single Seed Zone Testnet"}]}' + part = '{"label": "Testnet Single Zone", "blockheight": 2090512, "descriptor": "wpkh([990a73ad/84h/1h/0h]tpubDDHQMDnFdan2GyHBsG32VW9qiygbhVizGRTjiS3H79M49FSvpsvLXqLgp1yC7r43dXVHozWavi2Fc4WHUpZmQYmzoQbit28qJhLjScbAQWU/0/*)#ujr0xunp","devices": [{"type": "seedsigner", "label": "Single Seed Zone Testnet"}]}' d = DecodeQR(wordlist=bip39.WORDLIST) - d.addString(1_part) + d.add_data(part) assert d.qrType() == QRType.SPECTERWALLETQR assert d.isComplete() == True diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index c5b4048b..b6199443 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -60,7 +60,7 @@ def test_seedsigner_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), qr_type=QRType.SEEDSSQR, wordlist=bip39.WORDLIST) + e = EncodeQR(seed_phrase=mnemonic.split(" "), qr_type=QRType.SEEDQR, wordlist=bip39.WORDLIST) print(e.nextPart()) @@ -85,4 +85,14 @@ def test_specter_xpub_qr(): assert e.nextPart() == "p3of4 MxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1J" assert e.nextPart() == "p4of4 sZsfdBzsK3VUC5" +def test_ur_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.URXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.MEDIUM, wordlist=bip39.WORDLIST) + + assert e.nextPart() == "UR:CRYPTO-ACCOUNT/1-4/LPADAACSJKCYYNRDRTYAHDCAOEADCYSSMECPONAOLYTAADMETAADDLOXAXHDCLAOKSRLNLKPUEGYATHPMNRTAAOLAH" + assert e.nextPart() == "UR:CRYPTO-ACCOUNT/2-4/LPAOAACSJKCYYNRDRTYAHDCASNKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTBEAAHDCXVDTPMYQDLAWNGL" + assert e.nextPart() == "UR:CRYPTO-ACCOUNT/3-4/LPAXAACSJKCYYNRDRTYAHDCARSTDSPZSBZSPGERLGDATUYNLPYBTGYIYYKBTWTAOSWKSVTSGCHBYDKYAVDSEPDJLAT" + assert e.nextPart() == "UR:CRYPTO-ACCOUNT/4-4/LPAAAACSJKCYYNRDRTYAHDCAAMTAADDYOEADLOCSDYYKADYKAEYKAOYKAOCYSSMECPONAYCYIOREKKJKAECADRRKIE" diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py new file mode 100644 index 00000000..5c2b7d04 --- /dev/null +++ b/tests/test_seedqr.py @@ -0,0 +1,106 @@ +import os +import pyzbar +from embit import bip39 +from seedsigner.helpers.qr import QR +from seedsigner.helpers.ur2.bytewords import decode +from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus +from seedsigner.models.encode_qr import EncodeQR +from seedsigner.models.qr_type import QRType + + +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 + + e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type, wordlist=bip39.WORDLIST) + data = e.nextPart() + print(data) + + qr = QR() + image = qr.qrimage( + data=data, + width=240, + height=240, + border=3 + ) + + decoder = DecodeQR(wordlist=bip39.WORDLIST) + status = decoder.addImage(image) + assert status == DecodeQRStatus.COMPLETE + + decoded_seed_phrase = decoder.getSeedPhrase() + print(decoded_seed_phrase) + assert seed_phrase == decoded_seed_phrase + + + +def test_standard_seedqr_encode_decode_(): + """ Should encode 24- and 12- word mnemonics to Standard SeedQR format and decode + them back again to their original mnemonic seed phrase. + """ + # 24-word seed + run_encode_decode_test(os.urandom(32), mnemonic_length=24, qr_type=QRType.SEEDQR) + + # 12-word seed + run_encode_decode_test(os.urandom(16), mnemonic_length=12, qr_type=QRType.SEEDQR) + + + +def test_compact_seedqr_encode_decode(): + """ Should encode 24- and 12- word mnemonics to CompactSeedQR format and decode + them back again to their original mnemonic seed phrase. + """ + # 24-word seed + run_encode_decode_test(os.urandom(32), mnemonic_length=24, qr_type=QRType.COMPACTSEEDQR) + + # 12-word seed + run_encode_decode_test(os.urandom(16), mnemonic_length=12, qr_type=QRType.COMPACTSEEDQR) + + + +def test_compact_seedqr_handles_null_bytes(): + """ Should properly encode a CompactSeedQR with null bytes (b'\x00') in the input + entropy and decode it back to the original mnemonic seed. + """ + # 24-word seed, null bytes at the front + entropy = b'\x00' + os.urandom(31) + run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.COMPACTSEEDQR) + + # 24-word seed, null bytes in the middle + entropy = os.urandom(10) + b'\x00' + os.urandom(21) + run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.COMPACTSEEDQR) + + # 24-word seed, null bytes at the end + entropy = os.urandom(31) + b'\x00' + run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.COMPACTSEEDQR) + + # 24-word seed, multiple null bytes + entropy = os.urandom(5) + b'\x00' + os.urandom(5) + b'\x00' + os.urandom(20) + run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.COMPACTSEEDQR) + + # 24-word seed, multiple null bytes in a row + entropy = os.urandom(10) + b'\x00\x00' + os.urandom(20) + run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.COMPACTSEEDQR) + + # 12-word seed, null bytes at the beginning + entropy = b'\x00' + os.urandom(15) + run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.COMPACTSEEDQR) + + # 12-word seed, null bytes in the middle + entropy = os.urandom(5) + b'\x00' + os.urandom(10) + run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.COMPACTSEEDQR) + + # 12-word seed, null bytes at the end + entropy = os.urandom(15) + b'\x00' + run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.COMPACTSEEDQR) + + # 12-word seed, multiple null bytes + entropy = os.urandom(5) + b'\x00' + os.urandom(5) + b'\x00' + os.urandom(4) + run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.COMPACTSEEDQR) + + # 12-word seed, multiple null bytes in a row + entropy = os.urandom(10) + b'\x00\x00' + os.urandom(4) + run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.COMPACTSEEDQR)