def test_from_BIP32PathTemplate(self) -> None: p = BIP32PathTemplate("m/[4-44]h/[5-555555]h/1/4/*h") self.assertEqual(str(BIP32PathTemplate(p)), "m/[4-44]h/[5-555555]h/1/4/*h") self.assertEqual(str(BIP32PathTemplate(p, hardened_marker="'")), "m/[4-44]'/[5-555555]'/1/4/*'") p = BIP32PathTemplate("m/4'/5'/*/4") self.assertEqual(str(BIP32PathTemplate(p)), "m/4'/5'/*/4") self.assertEqual(str(BIP32PathTemplate(p, hardened_marker='h')), "m/4h/5h/*/4") p = BIP32PathTemplate("4'/5'/1/[3,4,5-10]") self.assertEqual(str(BIP32PathTemplate(p)), "4'/5'/1/[3-10]") self.assertEqual(str(BIP32PathTemplate(p, hardened_marker='h')), "4h/5h/1/[3-10]")
def test_tempate_as_list(self) -> None: self.assertEqual(list(BIP32PathTemplate('m/0')), [((0, 0), )]) self.assertEqual(list(BIP32PathTemplate('0')), [((0, 0), )]) self.assertEqual(list(BIP32PathTemplate('[0-10,12,59]/*')), [((0, 10), (12, 12), (59, 59)), ((0, BIP32_HARDENED_KEY_OFFSET - 1), )]) self.assertEqual(list(BIP32PathTemplate("m/4h/[5-10,15]/1h")), [ ((4 + BIP32_HARDENED_KEY_OFFSET, 4 + BIP32_HARDENED_KEY_OFFSET), ), ((5, 10), (15, 15)), ((1 + BIP32_HARDENED_KEY_OFFSET, 1 + BIP32_HARDENED_KEY_OFFSET), ) ]) self.assertEqual( list(BIP32PathTemplate("m/0'/2147483647'/1/10")), [((BIP32_HARDENED_KEY_OFFSET, BIP32_HARDENED_KEY_OFFSET), ), ((0xFFFFFFFF, 0xFFFFFFFF), ), ((1, 1), ), ((10, 10), )]) self.assertEqual( list( BIP32PathTemplate('m/' + '/'.join("%u'" % n for n in range(128)) + '/' + '/'.join("%u" % (BIP32_HARDENED_KEY_OFFSET - n - 1) for n in range(127)))), [((n + BIP32_HARDENED_KEY_OFFSET, n + BIP32_HARDENED_KEY_OFFSET), ) for n in range(128)] + [((BIP32_HARDENED_KEY_OFFSET - n - 1, BIP32_HARDENED_KEY_OFFSET - n - 1), ) for n in range(127)])
def test_random_access(self) -> None: p = BIP32Path("m/4h/5h/1/4") self.assertEqual(p[0], 4 + BIP32_HARDENED_KEY_OFFSET) self.assertEqual(p[1], 5 + BIP32_HARDENED_KEY_OFFSET) self.assertEqual(p[2], 1) self.assertEqual(p[3], 4) p = BIP32Path([0xFFFFFFFF - n for n in range(255)]) self.assertEqual(p[254], 0xFFFFFF01) pt = BIP32PathTemplate("m/4h/5h/[1-2]/[4,7]") self.assertEqual(pt[0][0][0], 4 + BIP32_HARDENED_KEY_OFFSET) self.assertEqual(pt[0][0][1], 4 + BIP32_HARDENED_KEY_OFFSET) self.assertEqual(pt[1][0][0], 5 + BIP32_HARDENED_KEY_OFFSET) self.assertEqual(pt[1][0][1], 5 + BIP32_HARDENED_KEY_OFFSET) self.assertEqual(pt[2][0][0], 1) self.assertEqual(pt[2][0][1], 2) self.assertEqual(pt[3][0][0], 4) self.assertEqual(pt[3][0][1], 4) self.assertEqual(pt[3][1][0], 7) self.assertEqual(pt[3][1][1], 7) pt = BIP32PathTemplate([[(0xFFFFFFFF - n, 0xFFFFFFFF - n)] for n in range(255)]) self.assertEqual(pt[254][0][0], 0xFFFFFF01) self.assertEqual(pt[254][0][1], 0xFFFFFF01)
def test_path_template_enforcement(self) -> None: xpriv1 = CCoinExtKey( 'xprv9s21ZrQH143K4TFwadu5VoGfAChTWXUw49YyTWE8SRqC9ZC9AQpHspzgbAcScTmC4MURiMT7pmCbci5oKbWijJmARiUeRiLXYehCtsoVdYf' ) xpriv2 = CCoinExtKey( 'xprv9s21ZrQH143K3QgBvK4tkeHuvuWc6KETTTcgGQ4NmW7g16AtCPV4hZpujiimpLM9ivFPgsMdNNVuVUnDwChutxczNKYHzP1Mo5HuqG7CNYv' ) assert xpriv2.derivation_info assert len(xpriv2.derivation_info.path) == 0 priv1 = CCoinKey( 'L27zAtDgjDC34sG5ZSey1wvdZ9JyZsNnvZEwbbZYWUYXXQtgri5R') xpub1 = CCoinExtPubKey( 'xpub69b6hm71WMe1PGpgUmaDPkbxYoTzpmswX8KGeinv7SPRcKT22RdMM4416kqtEUuXqXCAi7oGx7tHwCRTd3JHatE3WX1Zms6Lgj5mrbFyuro' ) xpub2 = xpriv2.derive(333).neuter() xpub1.assign_derivation_info( KeyDerivationInfo(xpub1.parent_fp, BIP32Path('m/0'))) pub1 = CPubKey( x('03b0fe9cfc88fed9fcecf9dcb7bb5c90dd1a4500f4cfc5c854ffc8e54d639d6bc5' )) xpub3 = xpub1.derive(0) xpub3.assign_derivation_info( KeyDerivationInfo(x('abcdef10'), BIP32Path('m/0/0'))) # No error when require_path_templates is not set KeyStore(xpriv1, xpriv2, priv1, xpub1, pub1, require_path_templates=False) with self.assertRaisesRegex(ValueError, 'only make sense for extended keys'): KeyStore((priv1, BIP32PathTemplate(''))) # type: ignore with self.assertRaisesRegex(ValueError, 'only make sense for extended keys'): KeyStore((pub1, [BIP32PathTemplate('')])) # type: ignore with self.assertRaisesRegex(ValueError, 'path templates must be specified'): KeyStore(xpriv1) with self.assertRaisesRegex(ValueError, 'path templates must be specified'): KeyStore(xpub1) # same but via add_key ks = KeyStore() with self.assertRaisesRegex(ValueError, 'only make sense for extended keys'): ks.add_key((priv1, BIP32PathTemplate(''))) # type: ignore with self.assertRaisesRegex(ValueError, 'only make sense for extended keys'): ks.add_key((pub1, [BIP32PathTemplate('')])) # type: ignore with self.assertRaisesRegex(ValueError, 'path templates list is empty'): ks.add_key((pub1, [])) # type: ignore with self.assertRaisesRegex(ValueError, 'only make sense for extended keys'): ks.add_key((pub1, '')) # type: ignore with self.assertRaisesRegex(ValueError, 'index template format is not valid'): ks.add_key((xpub1, 'abc')) # type: ignore with self.assertRaisesRegex(TypeError, 'is expected to be an instance of '): ks.add_key((xpub1, [10])) # type: ignore with self.assertRaisesRegex(ValueError, 'path templates must be specified'): ks.add_key(xpriv1) with self.assertRaisesRegex(ValueError, 'path templates must be specified'): ks.add_key(xpub1) # No error when path templates are specified for extended keys ks = KeyStore( (xpriv1, BIP32PathTemplate('m')), (xpriv2, 'm/[44,49,84]h/0h/0h/[0-1]/*'), (xpub1, ''), # '' same as BIP32PathTemplate('') (xpub2, ['0/1', 'm/333/3/33']), (xpub3, BIP32PathTemplate('m/0/0/1')), priv1, pub1) self.assertEqual(ks.get_privkey(priv1.pub.key_id), priv1) self.assertEqual(ks.get_pubkey(pub1.key_id), pub1) # still can find non-extended priv even if derivation info is # specified, because there's exact match. self.assertEqual( ks.get_privkey(priv1.pub.key_id, KeyDerivationInfo(xpriv1.parent_fp, BIP32Path("m"))), priv1) self.assertEqual(ks.get_pubkey(pub1.key_id), pub1) # can't find without derivation specified self.assertEqual(ks.get_privkey(xpriv1.pub.key_id), None) # but can find with derivation specified self.assertEqual( ks.get_privkey( xpriv1.pub.key_id, KeyDerivationInfo(xpriv1.fingerprint, BIP32Path('m'))), xpriv1.priv) # can't find without derivation specified self.assertEqual(ks.get_pubkey(xpub1.pub.key_id), None) # can find with derivation specified self.assertEqual( ks.get_pubkey(xpub1.pub.key_id, KeyDerivationInfo(xpub1.parent_fp, BIP32Path('m/0'))), xpub1.pub) # exception when derivation goes beyond template with self.assertRaises(BIP32PathTemplateViolation): ks.get_pubkey( xpub1.derive(1).pub.key_id, KeyDerivationInfo(xpub1.parent_fp, BIP32Path('m/0/1'))) # success when template allows self.assertEqual( ks.get_pubkey( xpub3.derive(1).pub.key_id, KeyDerivationInfo(x('abcdef10'), BIP32Path('m/0/0/1'))), xpub3.derive(1).pub) # fails when template not allows with self.assertRaises(BIP32PathTemplateViolation): ks.get_pubkey( xpub3.derive(2).pub.key_id, KeyDerivationInfo(x('abcdef10'), BIP32Path('m/0/0/2'))) long_path = BIP32Path( "m/43435/646/5677/5892/58885/2774/9943/75532/8888") with self.assertRaises(BIP32PathTemplateViolation): ks.get_privkey( xpriv2.derive_path(long_path).pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, long_path)) with self.assertRaises(BIP32PathTemplateViolation): ks.get_privkey( xpriv2.derive_path("44'/0'/0'/3/25").pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, BIP32Path('m/44h/0h/0h/3/25'))) with self.assertRaises(BIP32PathTemplateViolation): ks.get_privkey( xpriv2.derive_path("44'/0'/0'/0/1'").pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, BIP32Path('m/44h/0h/0h/0/1h'))) self.assertEqual( ks.get_privkey( xpriv2.derive_path("44'/0'/0'/1/25").pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, BIP32Path('m/44h/0h/0h/1/25'))), xpriv2.derive_path("44'/0'/0'/1/25").priv) with self.assertRaises(BIP32PathTemplateViolation): ks.get_pubkey( xpub2.derive_path('0').pub.key_id, KeyDerivationInfo(xpub2.parent_fp, BIP32Path('m/333/0'))) with self.assertRaises(BIP32PathTemplateViolation): ks.get_pubkey( xpub2.derive_path('3/34').pub.key_id, KeyDerivationInfo(xpub2.parent_fp, BIP32Path('m/333/3/34'))) self.assertEqual( ks.get_pubkey( xpub2.derive_path('3/33').pub.key_id, KeyDerivationInfo(xpub2.parent_fp, BIP32Path('m/333/3/33'))), xpub2.derive_path('3/33').pub) xpub49 = xpriv2.derive_path("m/49'/0'/0'/0").neuter() with self.assertRaisesRegex(ValueError, 'must specify full path'): ks = KeyStore( xpriv2, xpub49, default_path_template='[44,49,84]h/0h/0h/[0-1]/[0-50000]') ks = KeyStore( xpriv2, xpub49, default_path_template='m/[44,49,84]h/0h/0h/[0-1]/[0-50000]') with self.assertRaises(BIP32PathTemplateViolation): ks.get_privkey( xpriv2.derive_path(long_path).pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, long_path)) with self.assertRaises(BIP32PathTemplateViolation): ks.get_privkey( xpriv2.derive_path("44'/0'/0'/1/50001").pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, BIP32Path('m/44h/0h/0h/1/50001'))) self.assertEqual( ks.get_privkey( xpriv2.derive_path("44'/0'/0'/1/25").pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, BIP32Path('m/44h/0h/0h/1/25'))), xpriv2.derive_path("44'/0'/0'/1/25").priv) with self.assertRaises(BIP32PathTemplateViolation): ks.get_pubkey( xpub49.derive_path('50001').pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, BIP32Path('m/49h/0h/0h/0/50001'))) with self.assertRaises(BIP32PathTemplateViolation): ks.get_pubkey( xpub49.derive_path('50000/3').pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, BIP32Path('m/49h/0h/0h/0/50000/3'))) self.assertEqual( ks.get_pubkey( xpub49.derive_path('50000').pub.key_id, KeyDerivationInfo(xpriv2.fingerprint, BIP32Path('m/49h/0h/0h/0/50000'))), xpub49.derive_path('50000').pub)
select_chain_params('bitcoin/regtest') if args.input_file == '-': psbt_data = sys.stdin.read() else: with open(args.input_file, 'r') as f: psbt_data = f.read() path_template = None require_path_templates = True if args.path_template is not None: if args.without_path_template_checks: print('--without-path-template-checks conflicts with ' '--path-template argument') sys.exit(-1) path_template = BIP32PathTemplate(args.path_template) elif args.without_path_template_checks: require_path_templates = False keys = [] for key_index, key_data in enumerate(args.key or []): k: Optional[Union[CCoinKey, CCoinExtKey]] = None try: k = CCoinKey(key_data) except (ValueError, Base58Error): pass try: k = CCoinExtKey(key_data) except (ValueError, Base58Error): pass
def test_BIP32PathTemplate_with_generated_data(self) -> None: test_dict = load_path_teplate_test_vectors('bip32_template.json') for status, data in test_dict.items(): if status == "normal_finish": for elt in data: assert isinstance(elt, list) tmpl_str, tmpl = elt tmpl_tuple = tuple( tuple(tuple(range) for range in section) for section in json.loads(tmpl)) pt = BIP32PathTemplate(tmpl_str) self.assertEqual(tuple(pt), tmpl_tuple) else: for elt in data: assert isinstance(elt, str) tmpl_str = elt try: pt = BIP32PathTemplate(tmpl_str) except ValueError as e: if str(e).startswith( "incorrect path template index bound"): assert (status in [ "error_range_start_equals_end", "error_ranges_intersect", "error_range_order_bad" ]), (tmpl_str, status) elif str(e).startswith( "index range equals wildcard range"): assert (status == "error_range_equals_wildcard"), ( tmpl_str, status) elif str(e).startswith( "index template format is not valid"): assert (status in [ "error_unexpected_char", "error_invalid_char", "error_unexpected_finish", "error_digit_expected" ]), (tmpl_str, status) elif str(e).startswith( "leading zeroes are not allowed"): assert (status == "error_index_has_leading_zero" ), (tmpl_str, status) elif str(e).startswith( "index_from cannot be larger than index_to in an index tuple" ): assert (status == "error_range_order_bad"), ( tmpl_str, status) elif str(e).startswith( 'derivation path must not end with "/"'): assert (status == "error_unexpected_slash"), ( tmpl_str, status) elif str(e).startswith( 'partial derivation path must not start with "/"' ): assert (status == "error_unexpected_slash"), ( tmpl_str, status) elif str(e).startswith( 'duplicate slashes are not allowed'): assert (status == "error_unexpected_slash"), ( tmpl_str, status) elif str(e).startswith('Unexpected hardened marker'): assert ( status == "error_unexpected_hardened_marker" ), (tmpl_str, status) elif str(e).startswith('whitespace found'): assert (status == "error_unexpected_space"), ( tmpl_str, status) elif str(e).startswith( 'derivation index string cannot represent value > 2147483647' ): assert (status == "error_index_too_big"), ( tmpl_str, status) else: raise
def test_BIP32PathTemplate_match_path(self) -> None: t_partial = BIP32PathTemplate("4'/5'/1/[3,4,5-50]") t_full = BIP32PathTemplate("m/4'/5'/1/[3,4,5-50]") for v in [3, 4] + list(range(5, 50)): self.assertTrue(t_partial.match_path(BIP32Path(f"4'/5'/1/{v}"))) for v in [3, 4] + list(range(5, 50)): self.assertTrue(t_full.match_path(BIP32Path(f"m/4'/5'/1/{v}"))) self.assertFalse(t_full.match_path(BIP32Path("4'/5'/1/3"))) self.assertFalse(t_partial.match_path(BIP32Path("m/4'/5'/1/3"))) self.assertFalse(t_full.match_path(BIP32Path("m/4'/5'/1"))) self.assertFalse(t_partial.match_path(BIP32Path("4'/5'/1"))) self.assertFalse(t_full.match_path(BIP32Path("m/4'/5'/1/3/1"))) self.assertFalse(t_partial.match_path(BIP32Path("4'/5'/1/3/1"))) self.assertTrue( BIP32PathTemplate("m/4'/5'/1/[3,4,5-50]/*").match_path( BIP32Path("m/4'/5'/1/3/1"))) self.assertTrue( BIP32PathTemplate("4h/5h/1h/[3,4,5-50]h/*h").match_path( BIP32Path("4'/5'/1'/3'/323452'"))) self.assertFalse( BIP32PathTemplate("4h/5h/1h/[3,4,5-50]h/*h").match_path( BIP32Path("4'/5'/1'/3/323452'"))) self.assertFalse( BIP32PathTemplate("4h/5h/1h/[3,4,5-50]h/*h").match_path( BIP32Path("4'/5'/1'/3'/323452"))) self.assertTrue( BIP32PathTemplate("*h").match_path(BIP32Path("323452h"))) self.assertTrue( BIP32PathTemplate("[0-100,200-300]h").match_path(BIP32Path("99h"))) self.assertTrue( BIP32PathTemplate("[0-100,200-300]h").match_path( BIP32Path("299h"))) self.assertFalse( BIP32PathTemplate("[0-100,200-300]h").match_path( BIP32Path("199h"))) self.assertTrue(BIP32PathTemplate("m").match_path(BIP32Path("m"))) self.assertTrue(BIP32PathTemplate("").match_path(BIP32Path(""))) self.assertFalse(BIP32PathTemplate("m").match_path(BIP32Path(""))) self.assertFalse(BIP32PathTemplate("").match_path(BIP32Path("m"))) self.assertTrue( BIP32PathTemplate('/'.join(str(v) for v in range(255))).match_path( BIP32Path('/'.join(str(v) for v in range(255)))))
def test_path_template_from_list(self) -> None: with self.assertRaisesRegex(ValueError, 'cannot be negative'): BIP32PathTemplate([((-1, 0), )]) with self.assertRaisesRegex(ValueError, 'only increase'): BIP32PathTemplate([((10, 10), (10, 10))]) with self.assertRaisesRegex( ValueError, 'index_from cannot be larger than index_to'): BIP32PathTemplate([((10, 9), )]) with self.assertRaisesRegex(TypeError, 'is expected to be an instance of '): BIP32PathTemplate([(0, 0)]) # type: ignore with self.assertRaisesRegex(ValueError, 'derivation index cannot be'): BIP32PathTemplate([[(0xFFFFFFFF + 1, 0xFFFFFFFF + 2)] ]) # more than 32bit with self.assertRaisesRegex(ValueError, 'unsupported hardened_marker'): # only apostrophe and "h" markers are allowed BIP32PathTemplate([[(0xFFFFFFFF, 0xFFFFFFFF)], [(0, 0)], [(0x80000000, 0x80000000)]], hardened_marker='b') with self.assertRaisesRegex( ValueError, 'derivation path longer than 255 elements'): # too long path BIP32PathTemplate([((n, n), ) for n in range(256)]) self.assertEqual( str(BIP32PathTemplate([((0, 0), )], is_partial=False)), "m/0") self.assertEqual(str(BIP32PathTemplate([((0, 1), )], is_partial=True)), "[0-1]") self.assertEqual(str(BIP32PathTemplate([((0, 0), )], is_partial=True)), "0") self.assertEqual( str( BIP32PathTemplate([((0, BIP32_HARDENED_KEY_OFFSET - 1), )], is_partial=False)), "m/*") self.assertEqual( str( BIP32PathTemplate( [((BIP32_HARDENED_KEY_OFFSET, 0xFFFFFFFF), )], is_partial=False)), "m/*'") self.assertEqual( str( BIP32PathTemplate( [((BIP32_HARDENED_KEY_OFFSET, 0xFFFFFFFF), )], is_partial=False, hardened_marker='h')), "m/*h") self.assertEqual( str( BIP32PathTemplate( [((BIP32_HARDENED_KEY_OFFSET, 0xFFFFFFFF), )], is_partial=True, hardened_marker='h')), "*h") self.assertEqual(str(BIP32PathTemplate([], is_partial=False)), "m") self.assertEqual(str(BIP32PathTemplate([((0, 0), )])), "0") self.assertEqual(str(BIP32PathTemplate([])), "") self.assertEqual( str( BIP32PathTemplate([[(0xFFFFFFFF, 0xFFFFFFFF)], [(0x80000001, 0x80000001)], [(1, 2)], [(0x80000002, 0x80000003), (0x80000004, 0x80000004)]])), "2147483647'/1'/[1-2]/[2-3,4]'") self.assertEqual( str( BIP32PathTemplate([[(0xFFFFFFFF, 0xFFFFFFFF)], [(0x80000001, 0x80000001)], [(1, 2)], [(0x80000002, 0x80000003), (0x80000004, 0x80000004)]], hardened_marker='h', is_partial=False)), "m/2147483647h/1h/[1-2]/[2-3,4]h") self.assertEqual( str( BIP32PathTemplate([((n + BIP32_HARDENED_KEY_OFFSET, n + BIP32_HARDENED_KEY_OFFSET), ) for n in range(128)] + [((n, n), ) for n in range(127)], is_partial=False)), 'm/' + '/'.join("%u'" % n for n in range(128)) + '/' + '/'.join("%u" % n for n in range(127)))