def sign_with_descriptor(self, wname, d1, d2, all_sighashes=False): # to derive addresses desc1 = Descriptor.from_string(d1) desc2 = Descriptor.from_string(d2) # recv addr 2 addr1 = desc1.derive(2).address(NETWORKS['regtest']) # change addr 3 addr2 = desc2.derive(3).address(NETWORKS['regtest']) res = sim.query(f"bitcoin:{addr1}?index=2", [True]) # check it's found self.assertFalse(b"Can't find wallet" in res) # to add checksums d1 = rpc.getdescriptorinfo(d1)["descriptor"] d2 = rpc.getdescriptorinfo(d2)["descriptor"] rpc.createwallet(wname, True, True) w = rpc.wallet(wname) res = w.importmulti([{ "desc": d1, "internal": False, "timestamp": "now", "watchonly": True, "range": 10, },{ "desc": d2, "internal": True, "timestamp": "now", "watchonly": True, "range": 10, }],{"rescan": False}) self.assertTrue(all([k["success"] for k in res])) wdefault.sendtoaddress(addr1, 0.1) rpc.mine() psbt = w.walletcreatefundedpsbt([], [{wdefault.getnewaddress(): 0.002}], 0, {"includeWatching": True, "changeAddress": addr2}, True) unsigned = psbt["psbt"] sighashes = [None] if all_sighashes: sh = [SIGHASH.ALL, SIGHASH.NONE, SIGHASH.SINGLE] sighashes += sh + [s | SIGHASH.ANYONECANPAY for s in sh] tx = PSBT.from_base64(unsigned) for sh in sighashes: for inp in tx.inputs: inp.sighash_type = sh unsigned = tx.to_base64().encode() # confirm signing if sh in [SIGHASH.ALL, None]: signed = sim.query(b"sign "+unsigned, [True]) else: # confirm warning signed = sim.query(b"sign "+unsigned, [True, True]) # signed tx self.assertTrue(signed.startswith(b"cHNi")) combined = rpc.combinepsbt([unsigned.decode(), signed.decode()]) final = rpc.finalizepsbt(combined) self.assertTrue(final["complete"]) # broadcast res = rpc.testmempoolaccept([final["hex"]]) self.assertTrue(res[0]["allowed"])
def test_miniscript_compat(self): """Test we can parse Miniscript Descriptors created by other implementations""" generalistic_descs = [ "wsh(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)))#cwycq5x", "wsh(multi(2,xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*,xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))#n3cj9mhy", "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(6)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#76jsyzdg", ] for desc in generalistic_descs: Descriptor.from_string(desc)
def parse_key(masterpub: str): """Parses masterpub or descriptor and returns a tuple: (Descriptor, network) To create addresses use descriptor.derive(num).address(network=network) """ network = None # probably a single key if "(" not in masterpub: k = Key.from_string(masterpub) if not k.is_extended: raise ValueError("The key is not a master public key") if k.is_private: raise ValueError("Private keys are not allowed") # check depth if k.key.depth != 3: raise ValueError( "Non-standard depth. Only bip44, bip49 and bip84 are supported with bare xpubs. For custom derivation paths use descriptors." ) # if allowed derivation is not provided use default /{0,1}/* if k.allowed_derivation is None: k.allowed_derivation = AllowedDerivation.default() # get version bytes version = k.key.version for network_name in NETWORKS: net = NETWORKS[network_name] # not found in this network if version in [net["xpub"], net["ypub"], net["zpub"]]: network = net if version == net["xpub"]: desc = Descriptor.from_string("pkh(%s)" % str(k)) elif version == net["ypub"]: desc = Descriptor.from_string("sh(wpkh(%s))" % str(k)) elif version == net["zpub"]: desc = Descriptor.from_string("wpkh(%s)" % str(k)) break # we didn't find correct version if network is None: raise ValueError("Unknown master public key version") else: desc = Descriptor.from_string(masterpub) if not desc.is_wildcard: raise ValueError("Descriptor should have wildcards") for k in desc.keys: if k.is_extended: net = detect_network(k) if net is None: raise ValueError(f"Unknown version: {k}") if network is not None and network != net: raise ValueError("Keys from different networks") network = net return desc, network
def fill_output(cls, out: OutputScope, desc: Descriptor) -> bool: """ Fills derivations and all other information in PSBT output from derived descriptor """ if desc.script_pubkey() != out.script_pubkey: return False out.redeem_script = desc.redeem_script() out.witness_script = desc.witness_script() out.bip32_derivations = { key.get_public_key(): DerivationPath(key.origin.fingerprint, key.origin.derivation) for key in desc.keys } return True
def sign_with_descriptor(self, d1, d2, root): rpc = daemon.rpc wname = random_wallet_name() # to derive addresses desc1 = Descriptor.from_string(d1) desc2 = Descriptor.from_string(d2) # recv addr 2 addr1 = desc1.derive(2).address(net) # change addr 3 addr2 = desc2.derive(3).address(net) # to add checksums d1 = add_checksum(str(d1)) d2 = add_checksum(str(d2)) rpc.createwallet(wname, True, True) w = daemon.wallet(wname) res = w.importmulti([{ "desc": d1, "internal": False, "timestamp": "now", "watchonly": True, "range": 10, }, { "desc": d2, "internal": True, "timestamp": "now", "watchonly": True, "range": 10, }], {"rescan": False}) self.assertTrue(all([k["success"] for k in res])) wdefault = daemon.wallet() wdefault.sendtoaddress(addr1, 0.1) daemon.mine() psbt = w.walletcreatefundedpsbt([], [{ wdefault.getnewaddress(): 0.002 }], 0, { "includeWatching": True, "changeAddress": addr2 }, True) unsigned = psbt["psbt"] psbt = PSBT.from_string(unsigned) psbt.sign_with(root) final = rpc.finalizepsbt(str(psbt)) self.assertTrue(final["complete"]) # test accept res = rpc.testmempoolaccept([final["hex"]]) self.assertTrue(res[0]["allowed"])
def test_legacy_sh(self): cosigner = "[12345678/41h/2h/0h]tpubDCUwbXdGiV5qFWMyaHBfdtDv1AZtUJmENrLMGEooEdyYka1Gk5FrBdoTp54EFWxopWi9H7udD4d8CxMNa2GUECRCodbBkd4eZfiQzbMXWU3" path = "49h/1h/0h/2h" fgp = sim.query("fingerprint").decode() xpub = sim.query(f"xpub m/{path}").decode() d1 = f"sh(sortedmulti(1,[{fgp}/{path}]{xpub}/0/*,{cosigner}/0/*))" d2 = f"sh(sortedmulti(1,[{fgp}/{path}]{xpub}/1/*,{cosigner}/1/*))" # combined with default derivation {0,1}/* d3 = f"sh(sortedmulti(1,[{fgp}/{path}]{xpub}/" + "{0,1}/*" + f",{cosigner}/"+"{0,1}/*))" res = sim.query("addwallet legsh&"+d3, [True]) addr = Descriptor.from_string(d1).derive(5).address(NETWORKS['regtest']) # check it finds the wallet correctly sc = Descriptor.from_string(d1).derive(5).redeem_script().data.hex() res = sim.query(f"showaddr sh m/{path}/0/5 {sc}", [True]) self.assertEqual(res.decode(), addr) wname = wallet_prefix+"_legsh" self.sign_with_descriptor(wname, d1, d2)
def test_invalid(self): with self.assertRaises(Exception): Descriptor.from_string("wsh(tr(%s/0/*))" % ROOT) with self.assertRaises(Exception): Descriptor.from_string("sh(tr(%s/0/*))" % ROOT) # x-only is only allowed in tr Descriptor.from_string( "tr(b4ca2da5380d9aeb5ca67e4f18c487ae9b668748517e12b788496f63765e2efa)" ) with self.assertRaises(Exception): Descriptor.from_string( "wpkh(b4ca2da5380d9aeb5ca67e4f18c487ae9b668748517e12b788496f63765e2efa)" )
def test_descriptor(self): descstr = "tr(%s/0/*)" % ROOT.to_public() desc = Descriptor.from_string(descstr) self.assertTrue(desc.is_taproot) self.assertEqual(str(desc), descstr) for i, expected in enumerate(DERIVED_ADDRESSES): d = desc.derive(i) addr = d.address(NET) self.assertEqual(addr, expected) self.assertEqual(d.script_pubkey(), address_to_scriptpubkey(expected))
def test_no_derivation(self): """Default wallet without derivation path""" path = "84h/1h/0h" xpub = sim.query(f"xpub m/{path}").decode() d1 = f"wpkh({xpub}/0/*)" d2 = f"wpkh({xpub}/1/*)" wname = wallet_prefix+"_wpkh_noder" addr = Descriptor.from_string(d1).derive(5).address(NETWORKS['regtest']) self.sign_with_descriptor(wname, d1, d2)
def test_with_private(self): cosigner = "[12345678/41h/2h/0h]tprv8ffduPBLPgcNBZcXgCPo91WbmhXdpLKq8gtq7C6KnXbcqMPrj2NhSWp8WCjFarhBDi2TnPf7AcndKJZeF6Eq3uXXEbsbQZpk94cmrtopNH4" cosigner_public = "[12345678/41h/2h/0h]tpubDCMg3oDaY4J352eKZr4PYRAiLj3ZyfWjhzVcPi8dCoQ1fqedMRCHd1RzgLHXN7fbqMFCquaby2ETYKoVmjGLU7rdadZ2MeprqeHrJWHLKYn" cosigner2 = "cUHFVwhcD4jJnVQkuDw5MdhZQVf2t6qSF6miA7UDnDSyiBNSZ4st" cosigner2_public = "0307208358ae8ccce81c81ea7a89683ee854e1002e05be8df39cb0ca5ed44eac29" path = "49h/1h/0h/2h" fgp = sim.query("fingerprint").decode() xpub = sim.query(f"xpub m/{path}").decode() d1 = f"wsh(sortedmulti(3,[{fgp}/{path}]{xpub}/0/*,{cosigner_public}/0/*,"+f"{cosigner2_public}))" d2 = f"wsh(sortedmulti(3,[{fgp}/{path}]{xpub}/1/*,{cosigner_public}/1/*,"+f"{cosigner2_public}))" # combined with default derivation {0,1}/* d3 = f"wsh(sortedmulti(3,[{fgp}/{path}]{xpub}/" + "{0,1}/*" + f",{cosigner}/"+"{0,1}/*,"+f"{cosigner2}))" res = sim.query("addwallet wshpriv&"+d3, [True]) addr = Descriptor.from_string(d1).derive(5).address(NETWORKS['regtest']) # check it finds the wallet correctly sc = Descriptor.from_string(d1).derive(5).witness_script().data.hex() res = sim.query(f"showaddr wsh m/{path}/0/5 {sc}", [True]) self.assertEqual(res.decode(), addr) wname = wallet_prefix+"_wshprv" self.sign_with_descriptor(wname, d1, d2)
def main(): for dstr in classic_descriptors: # replace spaces and new lines dstr = dstr.replace("\n", "").replace(" ", "") # parse desc = Descriptor.from_string(dstr) # print(desc.policy) derived = desc.derive(12) print("Derived descriptor:\n%s" % derived) addr = derived.address(NETWORKS['test']) print("Address: %s\n" % addr) for dstr in paired_descriptors: # replace spaces and new lines dstr = dstr.replace("\n", "").replace(" ", "") # parse desc = Descriptor.from_string(dstr) # print(desc.policy) # 12-th address, testnet, 1st branch (change) # branch_index is the index of the allowed set, so if the set is {33,45} then branch_index=1 will derive using 45 index derived = desc.derive(12, branch_index=1) print("Derived descriptor:\n%s" % derived) addr = derived.address(NETWORKS['test']) print("Address: %s\n" % addr) for dstr in miniscript_descriptors: # replace spaces and new lines dstr = dstr.replace("\n", "").replace(" ", "") # parse desc = Descriptor.from_string(dstr) # print(desc.policy) # 12-th address, testnet, 1st branch (change) # if branch_index is not provided, we use the first index in all branches derived = desc.derive(12) print("Derived descriptor:\n%s" % derived) addr = derived.address(NETWORKS['test']) print("Address: %s\n" % addr)
def test_wpkh(self): """Native segwit single-sig""" path = "84h/1h/0h" fgp = sim.query("fingerprint").decode() xpub = sim.query(f"xpub m/{path}").decode() d1 = f"wpkh([{fgp}/{path}]{xpub}/0/*)" d2 = f"wpkh([{fgp}/{path}]{xpub}/1/*)" wname = wallet_prefix+"_wpkh" addr = Descriptor.from_string(d1).derive(5).address(NETWORKS['regtest']) # check it finds the wallet correctly res = sim.query(f"showaddr wpkh m/{path}/0/5", [True]) self.assertEqual(res.decode(), addr) self.sign_with_descriptor(wname, d1, d2)
def test_strange_derivation(self): path = "84h/1h/0h" fgp = sim.query("fingerprint").decode() xpub = sim.query(f"xpub m/{path}").decode() d1 = f"wpkh([{fgp}/{path}]{xpub}/44/8/*)" d2 = f"wpkh([{fgp}/{path}]{xpub}/55/8/*)" # combined with default derivation {0,1}/* d3 = f"wpkh([{fgp}/{path}]{xpub}"+ "/{44,55}/8/*)" res = sim.query("addwallet weird&"+d3, [True]) addr = Descriptor.from_string(d1).derive(5).address(NETWORKS['regtest']) # check it finds the wallet correctly res = sim.query(f"showaddr wpkh m/{path}/44/8/5", [True]) self.assertEqual(res.decode(), addr) wname = wallet_prefix+"_weird" self.sign_with_descriptor(wname, d1, d2)
def test_sh_wpkh(self): """Native segwit single-sig""" path = "49h/1h/0h" fgp = sim.query("fingerprint").decode() xpub = sim.query(f"xpub m/{path}").decode() d1 = f"sh(wpkh([{fgp}/{path}]{xpub}/0/*))" d2 = f"sh(wpkh([{fgp}/{path}]{xpub}/1/*))" # combined with default derivation {0,1}/* d3 = f"sh(wpkh([{fgp}/{path}]{xpub}"+ "/{0,1}/*))" wname = wallet_prefix+"_sh_wpkh" res = sim.query("addwallet shwpkh&"+d3, [True]) addr = Descriptor.from_string(d1).derive(5).address(NETWORKS['regtest']) # check it finds the wallet correctly res = sim.query(f"showaddr sh-wpkh m/{path}/0/5", [True]) self.assertEqual(res.decode(), addr) self.sign_with_descriptor(wname, d1, d2)
def test_sighashes_legacy(self): """Native segwit single-sig""" path = "44h/1h/0h" fgp = sim.query("fingerprint").decode() xpub = sim.query(f"xpub m/{path}").decode() # legacy sighashes d1 = f"pkh([{fgp}/{path}]{xpub}/0/*)" d2 = f"pkh([{fgp}/{path}]{xpub}/1/*)" d3 = f"pkh([{fgp}/{path}]{xpub}"+ "/{0,1}/*)" wname = wallet_prefix+"_pkhsighash" res = sim.query("addwallet pkhsighsh&"+d3, [True]) addr = Descriptor.from_string(d1).derive(5).address(NETWORKS['regtest']) # check it finds the wallet correctly res = sim.query(f"showaddr pkh m/{path}/0/5", [True]) self.assertEqual(res.decode(), addr) self.sign_with_descriptor(wname, d1, d2, all_sighashes=True)
def test_descriptors(self): keys = [ "[abcdef12/84h/22h]xpub6F6wWxm8F64iBHNhyaoh3QKCuuMUY5pfPPr1H1WuZXUXeXtZ21qjFN5ykaqnLL1jtPEFB9d94CyZrcYWKVdSiJKQ6mLGEB5sfrGFBpg6wgA/{0,1}/*", "03e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130", "[12345678/44h/12]xpub6BwcvdstHTJtLpp1WxUiQCYERWSB66XY5JrCpw71GAJxcJ6s2AiUoEK4Nzt6UDaTmanUiSe6TY2RoFturKNLXeWBhwBF6WBNghr8cr7qnjk/{0,1}/*", "[12345a78/42h/15]03e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130", ] dd = [ ("wsh(or_d(" "c:pk_k(020e0338c96a8870479f2396c373cc7696ba124e8635d41b0ea581112b67817261)," "c:pk_k(0250863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352)" "))", "21020e0338c96a8870479f2396c373cc7696ba124e8635d41b0ea581112b67817261ac7364210250863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352ac68" ), # # pkh - 8e5d7457d33a978d1c3c1e440f92a195e00cc7d8 # ("wsh(v:pk_h(03e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130))", None), ("sh(wsh(and_v(or_c(pk(%s),or_c(pk(%s),v:older(1000))),pk(%s))))" % tuple(keys[-3:]), "2103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130ac642103b8fa5d5959fa4027ccbf0736a86ccde4242e3051ea363437b4ff0d52598d7cecac6402e803b26968682103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130ac" ), ("sh(or_b(pk(%s),s:pk(%s)))" % tuple(keys[:2]), "2103801b3a4e3ca0d61d469445621561c47f6c1424d0fd353a44c2c3ebb84ae78f59ac7c2103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130ac9b" ), ("wsh(or_d(pk(%s),pkh(%s)))" % tuple(keys[-2:]), "2103b8fa5d5959fa4027ccbf0736a86ccde4242e3051ea363437b4ff0d52598d7cecac736476a9148e5d7457d33a978d1c3c1e440f92a195e00cc7d888ac68" ), ("wsh(and_v(v:pk(%s),or_d(pk(%s),older(12960))))" % tuple(keys[:2]), "2103801b3a4e3ca0d61d469445621561c47f6c1424d0fd353a44c2c3ebb84ae78f59ad2103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130ac736402a032b268" ), ("wsh(andor(pk(%s),older(1008),pk(%s)))" % tuple(keys[:2]), "2103801b3a4e3ca0d61d469445621561c47f6c1424d0fd353a44c2c3ebb84ae78f59ac642103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130ac6702f003b268" ), ("wsh(t:or_c(pk(%s),and_v(v:pk(%s),or_c(pk(%s),v:hash160(e7d285b4817f83f724cd29394da75dfc84fe639e)))))" % tuple(keys[:3]), "2103801b3a4e3ca0d61d469445621561c47f6c1424d0fd353a44c2c3ebb84ae78f59ac642103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130ad2103b8fa5d5959fa4027ccbf0736a86ccde4242e3051ea363437b4ff0d52598d7cecac6482012088a914e7d285b4817f83f724cd29394da75dfc84fe639e88686851" ), ("wsh(andor(pk(%s),or_i(and_v(v:pkh(%s),hash160(e7d285b4817f83f724cd29394da75dfc84fe639e)),older(1008)),pk(%s)))" % tuple(keys[:3]), "2103801b3a4e3ca0d61d469445621561c47f6c1424d0fd353a44c2c3ebb84ae78f59ac642103b8fa5d5959fa4027ccbf0736a86ccde4242e3051ea363437b4ff0d52598d7cecac676376a9148e5d7457d33a978d1c3c1e440f92a195e00cc7d888ad82012088a914e7d285b4817f83f724cd29394da75dfc84fe639e876702f003b26868" ), ("wsh(multi(2,%s,%s,%s))" % tuple(keys[:3]), "522103801b3a4e3ca0d61d469445621561c47f6c1424d0fd353a44c2c3ebb84ae78f592103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b141302103b8fa5d5959fa4027ccbf0736a86ccde4242e3051ea363437b4ff0d52598d7cec53ae" ), ("wsh(thresh(3,pk(%s),s:pk(%s),s:pk(%s),sdv:older(12960)))" % tuple(keys[:3]), "2103801b3a4e3ca0d61d469445621561c47f6c1424d0fd353a44c2c3ebb84ae78f59ac7c2103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b14130ac937c2103b8fa5d5959fa4027ccbf0736a86ccde4242e3051ea363437b4ff0d52598d7cecac937c766302a032b26968935387" ), ("wsh(multi(10," "0373b665b6fe153c5872de1344339ee60588491257d2c34567aa026af237143a6c," "02916ee61974fc4892afb2d3cad4c13472138b5521411de24a78910afb97b95f22," "0244efc096ea3b7df99071b1cfa1630144e20d8ccd1540e726034a051aa1802d3b," "02d9c51dc3f4088d5ce0b83f188fb14901b98c1c9e8cf771c49b7b441e56272b8a," "03094990a34af21ef3ed766c8e0cb1e44f5e0d80412bbe00a2ade82a024ca91d23," "02722a386ad0f6d7f1261808a3e70fab143303bd2264283486411c3183ea3ed1c3," "036070b1f2995d8ffda8478ef55affd39795689a3982d54b12180397b1ad1f5f75," "026515fa7603c10c44f6d316ae7592b5899d46d87ac1e574ec53de8b59f95efad6," "038c8f919f70062c084376223fd8b4f0c08958e70499df496411dde83a1bb64b0d," "02d0ea7084e344b56625277b074d15a15301b9d96b0b2dd9fc905e01fc3de408e1))", "5a210373b665b6fe153c5872de1344339ee60588491257d2c34567aa026af237143a6c2102916ee61974fc4892afb2d3cad4c13472138b5521411de24a78910afb97b95f22210244efc096ea3b7df99071b1cfa1630144e20d8ccd1540e726034a051aa1802d3b2102d9c51dc3f4088d5ce0b83f188fb14901b98c1c9e8cf771c49b7b441e56272b8a2103094990a34af21ef3ed766c8e0cb1e44f5e0d80412bbe00a2ade82a024ca91d232102722a386ad0f6d7f1261808a3e70fab143303bd2264283486411c3183ea3ed1c321036070b1f2995d8ffda8478ef55affd39795689a3982d54b12180397b1ad1f5f7521026515fa7603c10c44f6d316ae7592b5899d46d87ac1e574ec53de8b59f95efad621038c8f919f70062c084376223fd8b4f0c08958e70499df496411dde83a1bb64b0d2102d0ea7084e344b56625277b074d15a15301b9d96b0b2dd9fc905e01fc3de408e15aae" ), ("wsh(andor(" "multi(4," "036070b1f2995d8ffda8478ef55affd39795689a3982d54b12180397b1ad1f5f75," "026515fa7603c10c44f6d316ae7592b5899d46d87ac1e574ec53de8b59f95efad6," "038c8f919f70062c084376223fd8b4f0c08958e70499df496411dde83a1bb64b0d," "02d0ea7084e344b56625277b074d15a15301b9d96b0b2dd9fc905e01fc3de408e1)," "and_v(" "v:multi(6," "03856d447f1b890cc6e0e0114cd5bac58662c37ce7f458c458b72bd396597edfc7," "03e080e99896384aa8a07da837b2042a4c0d824eeaa8d51e6c9cff20682be75d4f," "02c6d258e728005d4d00e55ac4b87786df507921b3ba3efec244a47f4a2e61b4b0," "02edfc1d6088f9b6470ed4550d8bf2326ebebc0464a7f78581fa7283fc54edecf0," "02f3630d1f51b2ebaaf1c7ebae9c24318279d4cff5ad16cb290b6d26edf96dca9c," "0353ecc8e7b1cc90d405cd6fc9d9f24d44b6b5649abc2773f28a6ca4fa7a4cd629)," "older(144))," "thresh(5," "pkh(1ad3ca2d247b8e8888e41f89ac8bef217d83f33f)," "a:pkh(f94f2eadc9c1bc3a8b8c2c6364af2c070fd41206)," "a:pkh(3c306c2c97e4ba62ac0d7fb3965aba66b28e8959)," "a:pkh(ba7b9e846eb6b16420976c6bead54d9bb2b08d35)," "a:pkh(379ed952eb4740386acc59c2d28d9aa62e63968d)," "a:pkh(c30d2795e70b1ee6f8af0b33d9460d60cfcf10b3))))", "5421036070b1f2995d8ffda8478ef55affd39795689a3982d54b12180397b1ad1f5f7521026515fa7603c10c44f6d316ae7592b5899d46d87ac1e574ec53de8b59f95efad621038c8f919f70062c084376223fd8b4f0c08958e70499df496411dde83a1bb64b0d2102d0ea7084e344b56625277b074d15a15301b9d96b0b2dd9fc905e01fc3de408e154ae6476a9141ad3ca2d247b8e8888e41f89ac8bef217d83f33f88ac6b76a914f94f2eadc9c1bc3a8b8c2c6364af2c070fd4120688ac6c936b76a9143c306c2c97e4ba62ac0d7fb3965aba66b28e895988ac6c936b76a914ba7b9e846eb6b16420976c6bead54d9bb2b08d3588ac6c936b76a914379ed952eb4740386acc59c2d28d9aa62e63968d88ac6c936b76a914c30d2795e70b1ee6f8af0b33d9460d60cfcf10b388ac6c93558767562103856d447f1b890cc6e0e0114cd5bac58662c37ce7f458c458b72bd396597edfc72103e080e99896384aa8a07da837b2042a4c0d824eeaa8d51e6c9cff20682be75d4f2102c6d258e728005d4d00e55ac4b87786df507921b3ba3efec244a47f4a2e61b4b02102edfc1d6088f9b6470ed4550d8bf2326ebebc0464a7f78581fa7283fc54edecf02102f3630d1f51b2ebaaf1c7ebae9c24318279d4cff5ad16cb290b6d26edf96dca9c210353ecc8e7b1cc90d405cd6fc9d9f24d44b6b5649abc2773f28a6ca4fa7a4cd62956af029000b268" ), ("wsh(sortedmulti(2,%s,%s,%s))" % tuple(keys[:3]), "522103801b3a4e3ca0d61d469445621561c47f6c1424d0fd353a44c2c3ebb84ae78f592103b8fa5d5959fa4027ccbf0736a86ccde4242e3051ea363437b4ff0d52598d7cec2103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b1413053ae" ), ("wpkh(%s)" % keys[0], "0014f8f93df2160de8fd3ca716e2f905c74da3f9839f"), ("sh(wpkh(%s))" % keys[0], "0014f8f93df2160de8fd3ca716e2f905c74da3f9839f"), ("pkh(%s)" % keys[0], "76a914f8f93df2160de8fd3ca716e2f905c74da3f9839f88ac"), ] for i, (d, a) in enumerate(dd): s = BytesIO(d.encode()) sc = Descriptor.from_string(d) self.assertEqual(str(sc), d) # get top level script scc = sc.witness_script() or sc.redeem_script( ) or sc.script_pubkey() self.assertEqual(len(scc.data), sc.script_len) schex = hexlify(scc.data).decode() self.assertEqual(schex, a) self.assertEqual(str(sc), d)
API = "https://blockstream.info/testnet/api" # we will generate testnet addresses network = NETWORKS['test'] # after GAP_LIMIT addresses without any transactions # we will stop querying GAP_LIMIT = 20 # You can either provide a combined descriptor with {0,1} branches, # or iterate over descriptors (recv and change descriptors) # Seed: session spawn august alpha trap spider thing swim finish motor neutral across # Here we use combined descriptor (Bitcoin Core doesn't support that though) # Here we use vpub, but it's the same as this tpub: tpubDC93uE1NfJMF37r4EL87CHBUEtScESkBNo6Ym3DYCqKdmdtsL8ZqK39aHfaESmSn9ZohH1vzQjDchsuAXRDGXuowXZSXj3fY7PJ9yBAhWst # native segwit desc = Descriptor.from_string( "wpkh([911cf0a8/84h/1h/0h]vpub5Y6tmeqrefJq4jGy7RZmaBf6Zq44MpG7zwToqhNd6uoyv9bhkbPcvUAU1DaGvTBhYP3BAVDzxJgUF8BRhAY13zzSjHJNshNKyyaTS4F5hnr/{0,1}/*)" ) # desc = Descriptor.from_string("sh(wpkh([911cf0a8/49h/1h/0h]upub5EQTr2VnHFmuJc3btwZcETwhDCzvXF8hqK2AS57ZN5fzxa6LGMtoAKuyc12F7rBpKUyocqfc8kCTzHdTmJh1i6672pu3JzobNHu39oW9Btd/{0,1}/*))") # desc = Descriptor.from_string("pkh([911cf0a8/44h/1h/0h]tpubDD958ijpckkrCrWzY4jTwuCafkK1gijyJVbc96EViYN29Ac7K9eUyzSTrwQuoGUvwpzQMHh2fT8JGtnYHjTFWRXJAEs48s1nZSpG92hC1yb/{0,1}/*)") # where to send DESTINATION = "2N6AUY73q79SPzGvgPhR9biETV7DZffTQz9" # amount to send in sat AMOUNT = 10_000 # if change is less than DUST_LIMIT we don't create UTXO for it DUST_LIMIT = 100 # to speed up connection to the API s = requests.session() # last known block height, can be used as locktime in the transaction
def create_wallet(self, name, sigs_required, key_type, keys, devices): try: walletsindir = [ wallet["name"] for wallet in self.rpc.listwalletdir()["wallets"] ] except: walletsindir = [] wallet_alias = alias(name) i = 2 while (os.path.isfile( os.path.join(self.working_folder, "%s.json" % wallet_alias)) or os.path.join(self.rpc_path, wallet_alias) in walletsindir): wallet_alias = alias("%s %d" % (name, i)) i += 1 arr = key_type.split("-") descs = [key.metadata["combined"] for key in keys] recv_descs = ["%s/0/*" % desc for desc in descs] change_descs = ["%s/1/*" % desc for desc in descs] if len(keys) > 1: recv_descriptor = "sortedmulti({},{})".format( sigs_required, ",".join(recv_descs)) change_descriptor = "sortedmulti({},{})".format( sigs_required, ",".join(change_descs)) else: recv_descriptor = recv_descs[0] change_descriptor = change_descs[0] for el in arr[::-1]: recv_descriptor = "%s(%s)" % (el, recv_descriptor) change_descriptor = "%s(%s)" % (el, change_descriptor) if is_liquid(self.chain): blinding_key = None if len(devices) == 1: blinding_key = devices[0].blinding_key if not blinding_key: desc = Descriptor.from_string(recv_descriptor.split("#")[0]) # For now we use sha256(b"blinding_key", xor(chaincodes)) as a blinding key # where chaincodes are corresponding to xpub of the first receiving address xor = bytearray(32) desc_keys = desc.derive(0).keys for k in desc_keys: if k.is_extended: chaincode = k.key.chain_code for i in range(32): xor[i] = xor[i] ^ chaincode[i] secret = hashlib.sha256(b"blinding_key" + bytes(xor)).digest() blinding_key = ec.PrivateKey(secret).wif() if blinding_key: recv_descriptor = f"blinded(slip77({blinding_key}),{recv_descriptor})" change_descriptor = ( f"blinded(slip77({blinding_key}),{change_descriptor})") recv_descriptor = AddChecksum(recv_descriptor) change_descriptor = AddChecksum(change_descriptor) # v20.99 is pre-v21 Elements Core for descriptors if self.bitcoin_core_version_raw >= 209900: # Use descriptor wallet self.rpc.createwallet(os.path.join(self.rpc_path, wallet_alias), True, True, "", False, True) else: self.rpc.createwallet(os.path.join(self.rpc_path, wallet_alias), True) w = self.WalletClass( name, wallet_alias, "{} of {} {}".format(sigs_required, len(keys), purposes[key_type]) if len(keys) > 1 else purposes[key_type], addrtypes[key_type], "", -1, "", -1, 0, 0, recv_descriptor, change_descriptor, keys, devices, sigs_required, {}, [], os.path.join(self.working_folder, "%s.json" % wallet_alias), self.device_manager, self, ) # save wallet file to disk if w and self.working_folder is not None: w.save_to_file() # get Wallet class instance if w: self.wallets[name] = w return w else: raise ("Failed to create new wallet")
def test_miniscript(self): # and(pk(A),after(100)) -> and_v(v:pk(A),after(100)) path = "49h/1h/0h/2h" fgp = sim.query("fingerprint").decode() xpub = sim.query(f"xpub m/{path}").decode() desc = f"wsh(and_v(v:pk([{fgp}/{path}]{xpub}"+"/{0,1}/*),after(10)))" res = sim.query("addwallet mini&"+desc, [True]) wname = wallet_prefix+"_mini" d = Descriptor.from_string(desc) addr = d.derive(5).address(NETWORKS['regtest']) # check it finds the wallet correctly sc = d.derive(5).witness_script().data.hex() res = sim.query(f"showaddr wsh m/{path}/0/5 {sc}", [True]) self.assertEqual(res.decode(), addr) d1 = d.derive(2, branch_index=0) d2 = d.derive(3, branch_index=1) # recv addr 2 addr1 = d1.address(NETWORKS['regtest']) # change addr 3 addr2 = d2.address(NETWORKS['regtest']) res = sim.query(f"bitcoin:{addr1}?index=2", [True]) # check it's found self.assertFalse(b"Can't find wallet" in res) rpc.createwallet(wname, True, True) w = rpc.wallet(wname) res = w.importmulti([{ "scriptPubKey": {"address": addr1},#d1.script_pubkey().data.hex(), # "witnessscript": d1.witness_script().data.hex(), # "pubkeys": [k.sec().hex() for k in d1.keys], "internal": False, "timestamp": "now", "watchonly": True, },{ "scriptPubKey": {"address": addr2},#d2.script_pubkey().data.hex(), # "witnessscript": d2.witness_script().data.hex(), # "pubkeys": [k.sec().hex() for k in d2.keys], "internal": True, "timestamp": "now", "watchonly": True, }],{"rescan": False}) self.assertTrue(all([k["success"] for k in res])) wdefault.sendtoaddress(addr1, 0.1) rpc.mine() unspent = w.listunspent() self.assertTrue(len(unspent) > 0) unspent = [{"txid": u["txid"], "vout": u["vout"]} for u in unspent[:1]] tx = w.createrawtransaction(unspent, [{wdefault.getnewaddress(): 0.002},{addr2: 0.09799}]) psbt = PSBT.from_base64(w.converttopsbt(tx)) # locktime magic :) psbt.tx.locktime = 11 psbt.tx.vin[0].sequence = 10 # fillinig psbt with data psbt.inputs[0].witness_script = d1.witness_script() pub = ec.PublicKey.parse(d1.keys[0].sec()) psbt.inputs[0].bip32_derivations[pub] = DerivationPath(bytes.fromhex(fgp), bip32.parse_path(f"m/{path}")+[0,2]) tx = w.gettransaction(unspent[0]["txid"]) t = Transaction.from_string(tx["hex"]) psbt.inputs[0].witness_utxo = t.vout[unspent[0]["vout"]] psbt.outputs[1].witness_script = d2.witness_script() pub2 = ec.PublicKey.parse(d2.keys[0].sec()) psbt.outputs[1].bip32_derivations[pub2] = DerivationPath(bytes.fromhex(fgp), bip32.parse_path(f"m/{path}")+[1,3]) unsigned = psbt.to_base64() # confirm signing signed = sim.query("sign "+unsigned, [True]) stx = PSBT.from_base64(signed.decode()) # signed tx t = psbt.tx # print(stx) t.vin[0].witness = Witness([stx.inputs[0].partial_sigs[pub], psbt.inputs[0].witness_script.data]) # broadcast with self.assertRaises(Exception): res = rpc.sendrawtransaction(t.serialize().hex()) rpc.mine(11) res = rpc.sendrawtransaction(t.serialize().hex()) rpc.mine() self.assertEqual(len(bytes.fromhex(res)), 32)