async def sign_psbt(self, stream, show_screen): data = a2b_base64(stream.read()) psbt = PSBT.parse(data) wallets, meta = self.parse_psbt(psbt=psbt) spends = [] for w, amount in wallets: if w is None: name = "Unkown wallet" else: name = w.name spends.append("%.8f BTC\nfrom \"%s\"" % (amount / 1e8, name)) title = "Spending:\n" + "\n".join(spends) res = await show_screen(TransactionScreen(title, meta)) if res: for w, _ in wallets: if w is None: continue # fill derivation paths from proprietary fields w.update_gaps(psbt=psbt) w.save(self.keystore) psbt = w.fill_psbt(psbt, self.keystore.fingerprint) self.keystore.sign_psbt(psbt) # remove unnecessary stuff: out_psbt = PSBT(psbt.tx) for i, inp in enumerate(psbt.inputs): out_psbt.inputs[i].partial_sigs = inp.partial_sigs txt = b2a_base64(out_psbt.serialize()).decode().strip() return BytesIO(txt)
def test_psbt(self): """Basic signing of the PSBT""" clear_testdir() ks = get_keystore(mnemonic="ability "*11+"acid", password="") wapp = get_wallets_app(ks, 'regtest') # at this stage only wpkh wallet exists # so this tx be parsed and signed just fine unsigned, signed = PSBTS["wpkh"] psbt = PSBT.from_string(unsigned) # check it can sign b64-psbt s = BytesIO(psbt.to_string().encode()) self.assertTrue(wapp.can_process(s)) # check it can sign b64 with sign prefix s = BytesIO(b"sign "+psbt.to_string().encode()+b"\n\n") self.assertTrue(wapp.can_process(s)) # check it can sign raw psbt s = BytesIO(psbt.serialize()) self.assertTrue(wapp.can_process(s)) fout = BytesIO() wallets, meta = wapp.manager.preprocess_psbt(s, fout) # found a wallet self.assertEqual(len(wallets), 1) self.assertTrue(wapp.manager.wallets[0] in wallets) fout.seek(0) psbtv = PSBTView.view(fout) b = BytesIO() sig_count = wapp.manager.sign_psbtview(psbtv, b, wallets, None) self.assertEqual(PSBT.parse(b.getvalue()).to_string(), signed)
def test_invalid_psbts(self): for psbt_str in INVALID_VECTORS: psbt_bytes = unhexlify(psbt_str) try: PSBT.parse(psbt_bytes) except: continue # succeed if exception was thrown self.fail( "Invalid psbt was parsed successfully: {}".format(psbt_str))
def test_sign(self): """Parses a PSBT, signs both inputs (1 segwit and 1 legacy), and verifies each signature is correct""" xkey = HDKey.from_base58( "tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF" ) psbt_str = "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000" exp_partial_sigs = [ { "029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f": "3044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01", "02dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7": "3045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01", }, { "03089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc": "3044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01", "023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73": "3044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d201", }, ] psbt = PSBT.parse(unhexlify(psbt_str)) psbt.sign_with(xkey) for i in range(len(psbt.inputs)): inp = psbt.inputs[i] self.assertEqual(len(inp.partial_sigs), len(exp_partial_sigs[i])) for act_pub, act_sig in inp.partial_sigs.items(): act_pub_str = hexlify(act_pub.serialize()).decode("utf-8") self.assertIn(act_pub_str, exp_partial_sigs[i].keys()) self.assertEqual( hexlify(act_sig).decode("utf-8"), exp_partial_sigs[i][act_pub_str])
def test_valid_psbts(self): for psbt_str in VALID_VECTORS: psbt_bytes = unhexlify(psbt_str) psbt_act = PSBT.parse(psbt_bytes) msg = "Valid psbt changed after being parsed & serialized: {}".format( psbt_str) self.assertEqual(psbt_act.serialize(), psbt_bytes, msg)
async def sign_psbt(self, stream, show_screen): data = a2b_base64(stream.read()) psbt = PSBT.parse(data) wallet, meta = self.parse_psbt(psbt=psbt) res = await show_screen(TransactionScreen(wallet.name, meta)) if res: # fill derivation paths from proprietary fields wallet.update_gaps(psbt=psbt) wallet.save(self.keystore) psbt = wallet.fill_psbt(psbt, self.keystore.fingerprint) self.keystore.sign_psbt(psbt) # remove unnecessary stuff: out_psbt = PSBT(psbt.tx) for i, inp in enumerate(psbt.inputs): out_psbt.inputs[i].partial_sigs = inp.partial_sigs txt = b2a_base64(out_psbt.serialize()).decode().strip() return BytesIO(txt)
def test_revault_sign(self): """Basic signing of the PSBT""" for i, mnemonic in enumerate(MNEMONICS): clear_testdir() ks = get_keystore(mnemonic=mnemonic, password="") wapp = get_wallets_app(ks, 'main') # add wallets for wdesc in WALLETS: w = wapp.manager.parse_wallet(wdesc) wapp.manager.add_wallet(w) for j, (unsigned, signed, mnemonic_idx, wnames) in enumerate(PSBTS): psbt = PSBT.from_string(unsigned) s = BytesIO(psbt.to_string().encode()) # check it can sign b64-psbt self.assertTrue(wapp.can_process(s)) # check it can sign raw psbt s = BytesIO(psbt.serialize()) self.assertTrue(wapp.can_process(s)) fout = BytesIO() wallets, meta = wapp.manager.preprocess_psbt(s, fout) # check that we detected wallet and a non-standard sighash self.assertEqual([w.name for w in wallets], wnames) self.assertEqual([ inp.get("label").replace(" (watch-only)", "") for inp in meta["inputs"] ], wnames) self.assertEqual(meta["inputs"][0].get("sighash"), "ALL | ANYONECANPAY") fout.seek(0) psbtv = PSBTView.view(fout) b = BytesIO() if i not in mnemonic_idx: with self.assertRaises(WalletError): sig_count = wapp.manager.sign_psbtview( psbtv, b, wallets, None) elif i == mnemonic_idx[0]: sig_count = wapp.manager.sign_psbtview( psbtv, b, wallets, None) self.assertEqual( PSBT.parse(b.getvalue()).to_string(), signed)
async def sign_psbt(self, stream, show_screen, encoding=BASE64_STREAM): if encoding == BASE64_STREAM: data = a2b_base64(stream.read()) psbt = PSBT.parse(data) else: psbt = PSBT.read_from(stream) # check if all utxos are there and if there are custom sighashes sighash = SIGHASH.ALL custom_sighashes = [] for i, inp in enumerate(psbt.inputs): if inp.witness_utxo is None: if inp.non_witness_utxo is None: raise WalletError( "Invalid PSBT - missing previous transaction") if inp.sighash_type and inp.sighash_type != SIGHASH.ALL: custom_sighashes.append((i, inp.sighash_type)) if len(custom_sighashes) > 0: txt = [("Input %d: " % i) + SIGHASH_NAMES[sh] for (i, sh) in custom_sighashes] canceltxt = "Only sign ALL" if len(custom_sighashes) != len( psbt.inputs) else "Cancel" confirm = await show_screen( Prompt("Warning!", "\nCustom SIGHASH flags are used!\n\n" + "\n".join(txt), confirm_text="Sign anyway", cancel_text=canceltxt)) if confirm: sighash = None else: if len(custom_sighashes) == len(psbt.inputs): # nothing to sign return wallets, meta = self.parse_psbt(psbt=psbt) # there is an unknown wallet # wallet is a list of tuples: (wallet, amount) if None in [w[0] for w in wallets]: scr = Prompt( "Warning!", "\nUnknown wallet in inputs!\n\n\n" "Wallet for some inpunts is unknown! This means we can't verify change addresses.\n\n\n" "Hint:\nYou can cancel this transaction and import the wallet by scanning it's descriptor.\n\n\n" "Proceed to the transaction confirmation?", ) proceed = await show_screen(scr) if not proceed: return None spends = [] for w, amount in wallets: if w is None: name = "Unknown wallet" else: name = w.name spends.append('%.8f BTC\nfrom "%s"' % (amount / 1e8, name)) title = "Spending:\n" + "\n".join(spends) res = await show_screen(TransactionScreen(title, meta)) if res: self.show_loader(title="Signing transaction...") sigsStart = 0 for i, inp in enumerate(psbt.inputs): sigsStart += len(list(inp.partial_sigs.keys())) for w, _ in wallets: if w is None: continue # fill derivation paths from proprietary fields w.update_gaps(psbt=psbt) w.save(self.keystore) w.fill_psbt(psbt, self.keystore.fingerprint) if w.has_private_keys: w.sign_psbt(psbt, sighash) self.keystore.sign_psbt(psbt, sighash) # remove unnecessary stuff: out_psbt = PSBT(psbt.tx) sigsEnd = 0 for i, inp in enumerate(psbt.inputs): sigsEnd += len(list(inp.partial_sigs.keys())) out_psbt.inputs[i].partial_sigs = inp.partial_sigs del psbt gc.collect() if sigsEnd == sigsStart: raise WalletError( "We didn't add any signatures!\n\nMaybe you forgot to import the wallet?\n\nScan the wallet descriptor to import it." ) if encoding == BASE64_STREAM: txt = b2a_base64(out_psbt.serialize()).decode().strip() else: txt = out_psbt.serialize() return BytesIO(txt)