def htlc_tx(self, htlc, htlc_output_index, amount_msat, htlc_payment_preimage, remote_htlc_sig, success, cltv_timeout, local_feerate_per_kw, our_commit_tx): _script, our_htlc_tx_output = make_htlc_tx_output( amount_msat=amount_msat, local_feerate=local_feerate_per_kw, revocationpubkey=local_revocation_pubkey, local_delayedpubkey=local_delayedpubkey, success=success, to_self_delay=local_delay) our_htlc_tx_inputs = make_htlc_tx_inputs( htlc_output_txid=our_commit_tx.txid(), htlc_output_index=htlc_output_index, amount_msat=amount_msat, witness_script=bh2u(htlc)) our_htlc_tx = make_htlc_tx( cltv_expiry=cltv_timeout, inputs=our_htlc_tx_inputs, output=our_htlc_tx_output) local_sig = our_htlc_tx.sign_txin(0, local_privkey[:-1]) our_htlc_tx_witness = make_htlc_tx_witness( remotehtlcsig=bfh(remote_htlc_sig) + b"\x01", # 0x01 is SIGHASH_ALL localhtlcsig=bfh(local_sig), payment_preimage=htlc_payment_preimage if success else b'', # will put 00 on witness if timeout witness_script=htlc) our_htlc_tx._inputs[0]['witness'] = bh2u(our_htlc_tx_witness) return str(our_htlc_tx)
def mock_fork(self, bad_header): forkpoint = bad_header['block_height'] b = blockchain.Blockchain(config=self.config, forkpoint=forkpoint, parent=None, forkpoint_hash=bh2u(sha256(str(forkpoint))), prev_hash=bh2u(sha256(str(forkpoint - 1)))) return b
def __init__(self, window: 'ElectrumWindow', chan_id: bytes): super().__init__(window) # initialize instance fields self.window = window chan = self.chan = window.wallet.lnworker.channels[chan_id] self.format = lambda msat: window.format_amount_and_units(msat / 1000) # connect signals with slots self.ln_payment_completed.connect(self.do_ln_payment_completed) self.htlc_added.connect(self.do_htlc_added) # register callbacks for updating window.network.register_callback(self.ln_payment_completed.emit, ['ln_payment_completed']) window.network.register_callback(self.htlc_added.emit, ['htlc_added']) # set attributes of QDialog self.setWindowTitle(_('Channel Details')) self.setMinimumSize(800, 400) # add layouts vbox = QtWidgets.QVBoxLayout(self) form_layout = QtWidgets.QFormLayout(None) vbox.addLayout(form_layout) # add form content form_layout.addRow(_('Node ID:'), SelectableLabel(bh2u(chan.node_id))) form_layout.addRow(_('Channel ID:'), SelectableLabel(bh2u(chan.channel_id))) funding_label_text = f'<a href=click_destination>{chan.funding_outpoint.txid}</a>:{chan.funding_outpoint.output_index}' form_layout.addRow(_('Funding Outpoint:'), LinkedLabel(funding_label_text, self.show_tx)) form_layout.addRow(_('Short Channel ID:'), SelectableLabel(format_short_channel_id(chan.short_channel_id))) self.received_label = SelectableLabel() form_layout.addRow(_('Received (mSAT):'), self.received_label) self.sent_label = SelectableLabel() form_layout.addRow(_('Sent (mSAT):'), self.sent_label) self.htlc_minimum_msat = SelectableLabel(str(chan.config[REMOTE].htlc_minimum_msat)) form_layout.addRow(_('Minimum HTLC value accepted by peer (mSAT):'), self.htlc_minimum_msat) self.max_htlcs = SelectableLabel(str(chan.config[REMOTE].max_accepted_htlcs)) form_layout.addRow(_('Maximum number of concurrent HTLCs accepted by peer:'), self.max_htlcs) self.max_htlc_value = SelectableLabel(self.window.format_amount_and_units(chan.config[REMOTE].max_htlc_value_in_flight_msat / 1000)) form_layout.addRow(_('Maximum value of in-flight HTLCs accepted by peer:'), self.max_htlc_value) self.dust_limit = SelectableLabel(self.window.format_amount_and_units(chan.config[REMOTE].dust_limit_sat)) form_layout.addRow(_('Remote dust limit:'), self.dust_limit) self.reserve = SelectableLabel(self.window.format_amount_and_units(chan.config[REMOTE].reserve_sat)) form_layout.addRow(_('Remote channel reserve:'), self.reserve) # add htlc tree view to vbox (wouldn't scale correctly in QFormLayout) form_layout.addRow(_('Payments (HTLCs):'), None) w = QtWidgets.QTreeView(self) htlc_dict = chan.get_payments() w.setModel(self.make_model(htlc_dict)) w.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) vbox.addWidget(w) # initialize sent/received fields self.update_sent_received()
def sign_and_insert_remote_sig(self, tx, remote_pubkey, remote_signature, pubkey, privkey): assert type(remote_pubkey) is bytes assert len(remote_pubkey) == 33 assert type(remote_signature) is str assert type(pubkey) is bytes assert type(privkey) is bytes assert len(pubkey) == 33 assert len(privkey) == 33 tx.sign({bh2u(pubkey): (privkey[:-1], True)}) pubkeys, _x_pubkeys = tx.get_sorted_pubkeys(tx.inputs()[0]) index_of_pubkey = pubkeys.index(bh2u(remote_pubkey)) tx._inputs[0]["signatures"][index_of_pubkey] = remote_signature + "01" tx.raw = None
def test_mnemonic_to_seed_basic(self): # note: not a valid electrum seed seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic='foobar', passphrase='none') self.assertEqual( '741b72fd15effece6bfe5a26a52184f66811bd2be363190e07a42cca442b1a5bb22b3ad0eb338197287e6d314866c7fba863ac65d3f156087a5052ebc7157fce', bh2u(seed))
def append_lnaddr(self, it: HTLCItem, lnaddr: LnAddr): invoice = HTLCItem(_('Invoice')) invoice.appendRow([HTLCItem(_('Remote node public key')), HTLCItem(bh2u(lnaddr.pubkey.serialize()))]) invoice.appendRow([HTLCItem(_('Amount in sat')), HTLCItem(str(lnaddr.amount * COIN))]) # might have a comma because mSAT! invoice.appendRow([HTLCItem(_('Description')), HTLCItem(dict(lnaddr.tags).get('d', _('N/A')))]) invoice.appendRow([HTLCItem(_('Date')), HTLCItem(format_time(lnaddr.date))]) it.appendRow([invoice])
def gen_random_versioned_seed(cls): version = cls.LATEST_VERSION hex_seed = bh2u(os.urandom(16)) checksum = cls.code_hashid(version + hex_seed) return VersionedSeed(version=version.upper(), seed=hex_seed.upper(), checksum=checksum.upper())
def update(features): self.features = features set_label_enabled() if features.bootloader_hash: bl_hash = bh2u(features.bootloader_hash) bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) else: bl_hash = "N/A" noyes = [_("No"), _("Yes")] endis = [_("Enable Passphrases"), _("Disable Passphrases")] disen = [_("Disabled"), _("Enabled")] setchange = [_("Set a PIN"), _("Change PIN")] version = "%d.%d.%d" % (features.major_version, features.minor_version, features.patch_version) device_label.setText(features.label) pin_set_label.setText(noyes[features.pin_protection]) passphrases_label.setText(disen[features.passphrase_protection]) bl_hash_label.setText(bl_hash) label_edit.setText(features.label) device_id_label.setText(features.device_id) initialized_label.setText(noyes[features.initialized]) version_label.setText(version) clear_pin_button.setVisible(features.pin_protection) clear_pin_warning.setVisible(features.pin_protection) pin_button.setText(setchange[features.pin_protection]) pin_msg.setVisible(not features.pin_protection) passphrase_button.setText(endis[features.passphrase_protection]) language_label.setText(features.language)
def update(self, window): wallet = window.wallet if type(wallet) != Multisig_Wallet: return if self.listener is None: self.logger.info("starting listener") self.listener = Listener(self) self.listener.start() elif self.listener: self.logger.info("shutting down listener") self.listener.stop() self.listener = None self.keys = [] self.cosigner_list = [] for key, keystore in wallet.keystores.items(): xpub = keystore.get_master_public_key() pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes( compressed=True) _hash = bh2u(crypto.sha256d(pubkey)) if not keystore.is_watching_only(): self.keys.append((key, _hash, window)) else: self.cosigner_list.append((window, xpub, pubkey, _hash)) if self.listener: self.listener.set_keyhashes([t[1] for t in self.keys])
def on_qr(self, data): from electrum_gzro.bitcoin import base_decode, is_address data = data.strip() if is_address(data): self.set_URI(data) return if data.startswith('gravity:'): self.set_URI(data) return if data.startswith('ln'): self.set_ln_invoice(data) return # try to decode transaction from electrum_gzro.transaction import Transaction from electrum_gzro.util import bh2u try: text = bh2u(base_decode(data, None, base=43)) tx = Transaction(text) tx.deserialize() except: tx = None if tx: self.tx_dialog(tx) return # show error self.show_error("Unable to decode QR data")
def sign_transaction(self, tx: Transaction, password): # Build a PSBT in memory, upload it for signing. # - we can also work offline (without paired device present) if tx.is_complete(): return assert self.my_wallet, "Not clear which wallet associated with this Coldcard" client = self.get_client() assert client.dev.master_fingerprint == self.ckcc_xfp # makes PSBT required raw_psbt = build_psbt(tx, self.my_wallet) cc_finalize = not (type(self.my_wallet) is Multisig_Wallet) try: try: self.handler.show_message("Authorize Transaction...") client.sign_transaction_start(raw_psbt, cc_finalize) while 1: # How to kill some time, without locking UI? time.sleep(0.250) resp = client.sign_transaction_poll() if resp is not None: break rlen, rsha = resp # download the resulting txn. raw_resp = client.download_file(rlen, rsha) finally: self.handler.finished() except (CCUserRefused, CCBusyError) as exc: self.logger.info(f'Did not sign: {exc}') self.handler.show_error(str(exc)) return except BaseException as e: self.logger.exception('') self.give_error(e, True) return if cc_finalize: # We trust the coldcard to re-serialize final transaction ready to go tx.update(bh2u(raw_resp)) else: # apply partial signatures back into txn psbt = BasicPSBT() psbt.parse(raw_resp, client.label()) merge_sigs_from_psbt(tx, psbt)
def open(self, *args, **kwargs): super(LightningOpenChannelDialog, self).open(*args, **kwargs) if self.lnaddr: fee = self.app.electrum_config.fee_per_kb() if not fee: fee = config.FEERATE_FALLBACK_STATIC_FEE self.amount = self.app.format_amount_and_units(self.lnaddr.amount * COIN + fee * 2) self.pubkey = bh2u(self.lnaddr.pubkey.serialize()) if self.msg: self.app.show_info(self.msg)
def test_mnemonic_to_seed(self): for test_name, test in SEED_TEST_CASES.items(): if test.words_hex is not None: self.assertEqual(test.words_hex, bh2u(test.words.encode('utf8')), msg=test_name) self.assertTrue(is_new_seed(test.words, prefix=test.seed_version), msg=test_name) m = mnemonic.Mnemonic(lang=test.lang) if test.entropy is not None: self.assertEqual(test.entropy, m.mnemonic_decode(test.words), msg=test_name) if test.passphrase_hex is not None: self.assertEqual(test.passphrase_hex, bh2u(test.passphrase.encode('utf8')), msg=test_name) seed = mnemonic.Mnemonic.mnemonic_to_seed( mnemonic=test.words, passphrase=test.passphrase) self.assertEqual(test.bip32_seed, bh2u(seed), msg=test_name)
def sign_transaction(self, keystore, tx, prev_tx, xpub_path): self.prev_tx = prev_tx self.xpub_path = xpub_path client = self.get_client(keystore) inputs = self.tx_inputs(tx, True) outputs = self.tx_outputs(keystore.get_derivation(), tx) signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime, version=tx.version)[0] signatures = [(bh2u(x) + '01') for x in signatures] tx.update_signatures(signatures)
def new_channel_dialog(self): lnworker = self.parent.wallet.lnworker d = WindowModalDialog(self.parent, _('Open Channel')) d.setMinimumWidth(700) vbox = QVBoxLayout(d) h = QGridLayout() local_nodeid = QLineEdit() local_nodeid.setText(bh2u(lnworker.node_keypair.pubkey)) local_nodeid.setReadOnly(True) local_nodeid.setCursorPosition(0) remote_nodeid = QLineEdit() local_amt_inp = BTCAmountEdit(self.parent.get_decimal_point) local_amt_inp.setAmount(200000) push_amt_inp = BTCAmountEdit(self.parent.get_decimal_point) push_amt_inp.setAmount(0) h.addWidget(QLabel(_('Your Node ID')), 0, 0) h.addWidget(local_nodeid, 0, 1) h.addWidget( QLabel(_('Remote Node ID or connection string or invoice')), 1, 0) h.addWidget(remote_nodeid, 1, 1) h.addWidget(QLabel('Local amount'), 2, 0) h.addWidget(local_amt_inp, 2, 1) h.addWidget(QLabel('Push amount'), 3, 0) h.addWidget(push_amt_inp, 3, 1) vbox.addLayout(h) ok_button = OkButton(d) ok_button.setDefault(True) vbox.addLayout(Buttons(CancelButton(d), ok_button)) suggestion = lnworker.suggest_peer() or b'' remote_nodeid.setText(bh2u(suggestion)) remote_nodeid.setCursorPosition(0) if not d.exec_(): return local_amt = local_amt_inp.get_amount() push_amt = push_amt_inp.get_amount() connect_contents = str(remote_nodeid.text()).strip() self.parent.open_channel(connect_contents, local_amt, push_amt)
def sign_transaction(self, keystore, tx, prev_tx, xpub_path): prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx, xpub_path) for txhash, tx in prev_tx.items() } client = self.get_client(keystore) inputs = self.tx_inputs(tx, xpub_path, True) outputs = self.tx_outputs(keystore.get_derivation(), tx) details = SignTx(lock_time=tx.locktime, version=tx.version) signatures, _ = client.sign_tx(self.get_coin_name(), inputs, outputs, details=details, prev_txes=prev_tx) signatures = [(bh2u(x) + '01') for x in signatures] tx.update_signatures(signatures)
def on_receive(self, keyhash, message): self.logger.info(f"signal arrived for {keyhash}") for key, _hash, window in self.keys: if _hash == keyhash: break else: self.logger.info("keyhash not found") return wallet = window.wallet if isinstance(wallet.keystore, keystore.Hardware_KeyStore): window.show_warning( _('An encrypted transaction was retrieved from cosigning pool.' ) + '\n' + _('However, hardware wallets do not support message decryption, ' 'which makes them not compatible with the current design of cosigner pool.' )) return elif wallet.has_keystore_encryption(): password = window.password_dialog( _('An encrypted transaction was retrieved from cosigning pool.' ) + '\n' + _('Please enter your password to decrypt it.')) if not password: return else: password = None if not window.question( _("An encrypted transaction was retrieved from cosigning pool." ) + '\n' + _("Do you want to open it now?")): return xprv = wallet.keystore.get_master_private_key(password) if not xprv: return try: privkey = BIP32Node.from_xkey(xprv).eckey message = bh2u(privkey.decrypt_message(message)) except Exception as e: self.logger.exception('') window.show_error(_('Error decrypting message') + ':\n' + repr(e)) return self.listener.clear(keyhash) tx = transaction.Transaction(message) show_transaction(tx, window, prompt_if_unsaved=True)
def test_push_script(self): # https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#push-operators self.assertEqual(push_script(''), bh2u(bytes([opcodes.OP_0]))) self.assertEqual(push_script('07'), bh2u(bytes([opcodes.OP_7]))) self.assertEqual(push_script('10'), bh2u(bytes([opcodes.OP_16]))) self.assertEqual(push_script('81'), bh2u(bytes([opcodes.OP_1NEGATE]))) self.assertEqual(push_script('11'), '0111') self.assertEqual(push_script(75 * '42'), '4b' + 75 * '42') self.assertEqual( push_script(76 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('4c' + 76 * '42'))) self.assertEqual( push_script(100 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('64' + 100 * '42'))) self.assertEqual( push_script(255 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('ff' + 255 * '42'))) self.assertEqual( push_script(256 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA2]) + bfh('0001' + 256 * '42'))) self.assertEqual( push_script(520 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA2]) + bfh('0802' + 520 * '42')))
def format_fields(self, chan): labels = {} for subject in (REMOTE, LOCAL): bal_minus_htlcs = chan.balance_minus_outgoing_htlcs( subject) // 1000 label = self.parent.format_amount(bal_minus_htlcs) other = subject.inverted() bal_other = chan.balance(other) // 1000 bal_minus_htlcs_other = chan.balance_minus_outgoing_htlcs( other) // 1000 if bal_other != bal_minus_htlcs_other: label += ' (+' + self.parent.format_amount( bal_other - bal_minus_htlcs_other) + ')' labels[subject] = label return [ format_short_channel_id(chan.short_channel_id), bh2u(chan.node_id), labels[LOCAL], labels[REMOTE], chan.get_state() ]
def sign_transaction(self, tx: Transaction, password): if tx.is_complete(): return client = self.get_client() inputs = [] inputsPaths = [] pubKeys = [] chipInputs = [] redeemScripts = [] signatures = [] changePath = "" output = None p2shTransaction = False segwitTransaction = False pin = "" self.get_client() # prompt for the PIN before displaying the dialog if necessary # Fetch inputs of the transaction to sign derivations = self.get_tx_derivations(tx) for txin in tx.inputs(): if txin['type'] == 'coinbase': self.give_error("Coinbase not supported") # should never happen if txin['type'] in ['p2sh']: p2shTransaction = True if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']: if not self.get_client_electrum().supports_segwit(): self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) segwitTransaction = True if txin['type'] in ['p2wpkh', 'p2wsh']: if not self.get_client_electrum().supports_native_segwit(): self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) segwitTransaction = True pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) for i, x_pubkey in enumerate(x_pubkeys): if x_pubkey in derivations: signingPos = i s = derivations.get(x_pubkey) hwAddress = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1]) break else: self.give_error("No matching x_key for sign_transaction") # should never happen redeemScript = Transaction.get_preimage_script(txin) txin_prev_tx = txin.get('prev_tx') if txin_prev_tx is None and not Transaction.is_segwit_input(txin): raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None inputs.append([txin_prev_tx_raw, txin['prevout_n'], redeemScript, txin['prevout_hash'], signingPos, txin.get('sequence', 0xffffffff - 1), txin.get('value')]) inputsPaths.append(hwAddress) pubKeys.append(pubkeys) # Sanity check if p2shTransaction: for txin in tx.inputs(): if txin['type'] != 'p2sh': self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen txOutput = var_int(len(tx.outputs())) for o in tx.outputs(): output_type, addr, amount = o.type, o.address, o.value txOutput += int_to_hex(amount, 8) script = tx.pay_script(output_type, addr) txOutput += var_int(len(script)//2) txOutput += script txOutput = bfh(txOutput) # Recognize outputs # - only one output and one change is authorized (for hw.1 and nano) # - at most one output can bypass confirmation (~change) (for all) if not p2shTransaction: if not self.get_client_electrum().supports_multi_output(): if len(tx.outputs()) > 2: self.give_error("Transaction with more than 2 outputs not supported") has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) for o in tx.outputs(): assert o.type == TYPE_ADDRESS info = tx.output_info.get(o.address) if (info is not None) and len(tx.outputs()) > 1 \ and not has_change: index = info.address_index # prioritise hiding outputs on the 'change' branch from user # because no more than one change address allowed if info.is_change == any_output_on_change_branch: changePath = self.get_derivation()[2:] + "/%d/%d"%index has_change = True else: output = o.address else: output = o.address self.handler.show_message(_("Confirm Transaction on your Ledger device...")) try: # Get trusted inputs from the original transactions for utxo in inputs: sequence = int_to_hex(utxo[5], 4) if segwitTransaction: tmp = bfh(utxo[3])[::-1] tmp += bfh(int_to_hex(utxo[1], 4)) tmp += bfh(int_to_hex(utxo[6], 8)) # txin['value'] chipInputs.append({'value' : tmp, 'witness' : True, 'sequence' : sequence}) redeemScripts.append(bfh(utxo[2])) elif not p2shTransaction: txtmp = bitcoinTransaction(bfh(utxo[0])) trustedInput = self.get_client().getTrustedInput(txtmp, utxo[1]) trustedInput['sequence'] = sequence chipInputs.append(trustedInput) redeemScripts.append(txtmp.outputs[utxo[1]].script) else: tmp = bfh(utxo[3])[::-1] tmp += bfh(int_to_hex(utxo[1], 4)) chipInputs.append({'value' : tmp, 'sequence' : sequence}) redeemScripts.append(bfh(utxo[2])) # Sign all inputs firstTransaction = True inputIndex = 0 rawTx = tx.serialize_to_network() self.get_client().enableAlternate2fa(False) if segwitTransaction: self.get_client().startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) outputData['outputData'] = txOutput if outputData['confirmationNeeded']: outputData['address'] = output self.handler.finished() pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin if not pin: raise UserWarning() self.handler.show_message(_("Confirmed. Signing Transaction...")) while inputIndex < len(inputs): singleInput = [ chipInputs[inputIndex] ] self.get_client().startUntrustedTransaction(False, 0, singleInput, redeemScripts[inputIndex], version=tx.version) inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ signatures.append(inputSignature) inputIndex = inputIndex + 1 else: while inputIndex < len(inputs): self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) outputData['outputData'] = txOutput if outputData['confirmationNeeded']: outputData['address'] = output self.handler.finished() pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin if not pin: raise UserWarning() self.handler.show_message(_("Confirmed. Signing Transaction...")) else: # Sign input with the provided PIN inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ signatures.append(inputSignature) inputIndex = inputIndex + 1 firstTransaction = False except UserWarning: self.handler.show_error(_('Cancelled by user')) return except BTChipException as e: if e.sw in (0x6985, 0x6d00): # cancelled by user return elif e.sw == 0x6982: raise # pin lock. decorator will catch it else: self.logger.exception('') self.give_error(e, True) except BaseException as e: self.logger.exception('') self.give_error(e, True) finally: self.handler.finished() for i, txin in enumerate(tx.inputs()): signingPos = inputs[i][4] tx.add_signature_to_txin(i, signingPos, bh2u(signatures[i])) tx.raw = tx.serialize()
def make_htlc_item(self, i: UpdateAddHtlc, direction: Direction) -> HTLCItem: it = HTLCItem(_('Sent HTLC with ID {}' if Direction.SENT == direction else 'Received HTLC with ID {}').format(i.htlc_id)) it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format(i.amount_msat))]) it.appendRow([HTLCItem(_('CLTV expiry')),HTLCItem(str(i.cltv_expiry))]) it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(bh2u(i.payment_hash))]) return it
def build_psbt(tx: Transaction, wallet: Abstract_Wallet): # Render a PSBT file, for possible upload to Coldcard. # # TODO this should be part of Wallet object, or maybe Transaction? if getattr(tx, 'raw_psbt', False): _logger.info('PSBT cache hit') return tx.raw_psbt inputs = tx.inputs() if 'prev_tx' not in inputs[0]: # fetch info about inputs, if needed? # - needed during export PSBT flow, not normal online signing wallet.add_hw_info(tx) # wallet.add_hw_info installs this attr assert tx.output_info is not None, 'need data about outputs' # Build a map of all pubkeys needed as derivation from master XFP, in PSBT binary format # 1) binary version of the common subpath for all keys # m/ => fingerprint LE32 # a/b/c => ints # # 2) all used keys in transaction: # - for all inputs and outputs (when its change back) # - for all keystores, if multisig # subkeys = {} for ks in wallet.get_keystores(): # XFP + fixed prefix for this keystore ks_prefix = packed_xfp_path_for_keystore(ks) # all pubkeys needed for input signing for xpubkey, derivation in ks.get_tx_derivations(tx).items(): pubkey = xpubkey_to_pubkey(xpubkey) # assuming depth two, non-harded: change + index aa, bb = derivation assert 0 <= aa < 0x80000000 and 0 <= bb < 0x80000000 subkeys[bfh(pubkey)] = ks_prefix + pack('<II', aa, bb) # all keys related to change outputs for o in tx.outputs(): if o.address in tx.output_info: # this address "is_mine" but might not be change (if I send funds to myself) output_info = tx.output_info.get(o.address) if not output_info.is_change: continue chg_path = output_info.address_index assert chg_path[0] == 1 and len(chg_path) == 2, f"unexpected change path: {chg_path}" pubkey = ks.derive_pubkey(True, chg_path[1]) subkeys[bfh(pubkey)] = ks_prefix + pack('<II', *chg_path) for txin in inputs: assert txin['type'] != 'coinbase', _("Coinbase not supported") if txin['type'] in ['p2sh', 'p2wsh-p2sh', 'p2wsh']: assert type(wallet) is Multisig_Wallet # Construct PSBT from start to finish. out_fd = io.BytesIO() out_fd.write(b'psbt\xff') def write_kv(ktype, val, key=b''): # serialize helper: write w/ size and key byte out_fd.write(my_var_int(1 + len(key))) out_fd.write(bytes([ktype]) + key) if isinstance(val, str): val = bfh(val) out_fd.write(my_var_int(len(val))) out_fd.write(val) # global section: just the unsigned txn class CustomTXSerialization(Transaction): @classmethod def input_script(cls, txin, estimate_size=False): return '' unsigned = bfh(CustomTXSerialization(tx.serialize()).serialize_to_network(witness=False)) write_kv(PSBT_GLOBAL_UNSIGNED_TX, unsigned) if type(wallet) is Multisig_Wallet: # always put the xpubs into the PSBT, useful at least for checking for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()): ks_prefix = packed_xfp_path_for_keystore(ks) write_kv(PSBT_GLOBAL_XPUB, ks_prefix, DecodeBase58Check(xp)) # end globals section out_fd.write(b'\x00') # inputs section for txin in inputs: if Transaction.is_segwit_input(txin): utxo = txin['prev_tx'].outputs()[txin['prevout_n']] spendable = txin['prev_tx'].serialize_output(utxo) write_kv(PSBT_IN_WITNESS_UTXO, spendable) else: write_kv(PSBT_IN_NON_WITNESS_UTXO, str(txin['prev_tx'])) pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) pubkeys = [bfh(k) for k in pubkeys] if type(wallet) is Multisig_Wallet: # always need a redeem script for multisig scr = Transaction.get_preimage_script(txin) if Transaction.is_segwit_input(txin): # needed for both p2wsh-p2sh and p2wsh write_kv(PSBT_IN_WITNESS_SCRIPT, bfh(scr)) else: write_kv(PSBT_IN_REDEEM_SCRIPT, bfh(scr)) sigs = txin.get('signatures') for pk_pos, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)): if pubkey in subkeys: # faster? case ... calculated above write_kv(PSBT_IN_BIP32_DERIVATION, subkeys[pubkey], pubkey) else: # when an input is partly signed, tx.get_tx_derivations() # doesn't include that keystore's value and yet we need it # because we need to show a correct keypath... assert x_pubkey[0:2] == 'ff', x_pubkey for ks in wallet.get_keystores(): d = ks.get_pubkey_derivation(x_pubkey) if d is not None: ks_path = packed_xfp_path_for_keystore(ks, d) write_kv(PSBT_IN_BIP32_DERIVATION, ks_path, pubkey) break else: raise AssertionError("no keystore for: %s" % x_pubkey) if txin['type'] == 'p2wpkh-p2sh': assert len(pubkeys) == 1, 'can be only one redeem script per input' pa = hash_160(pubkey) assert len(pa) == 20 write_kv(PSBT_IN_REDEEM_SCRIPT, b'\x00\x14'+pa) # optional? insert (partial) signatures that we already have if sigs and sigs[pk_pos]: write_kv(PSBT_IN_PARTIAL_SIG, bfh(sigs[pk_pos]), pubkey) out_fd.write(b'\x00') # outputs section for o in tx.outputs(): # can be empty, but must be present, and helpful to show change inputs # wallet.add_hw_info() adds some data about change outputs into tx.output_info if o.address in tx.output_info: # this address "is_mine" but might not be change (if I send funds to myself) output_info = tx.output_info.get(o.address) if output_info.is_change: pubkeys = [bfh(i) for i in wallet.get_public_keys(o.address)] # Add redeem/witness script? if type(wallet) is Multisig_Wallet: # always need a redeem script for multisig cases scr = bfh(multisig_script([bh2u(i) for i in sorted(pubkeys)], wallet.m)) if output_info.script_type == 'p2wsh-p2sh': write_kv(PSBT_OUT_WITNESS_SCRIPT, scr) write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x20' + sha256(scr)) elif output_info.script_type == 'p2wsh': write_kv(PSBT_OUT_WITNESS_SCRIPT, scr) elif output_info.script_type == 'p2sh': write_kv(PSBT_OUT_REDEEM_SCRIPT, scr) else: raise ValueError(output_info.script_type) elif output_info.script_type == 'p2wpkh-p2sh': # need a redeem script when P2SH is used to wrap p2wpkh assert len(pubkeys) == 1 pa = hash_160(pubkeys[0]) write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x14' + pa) # Document change output's bip32 derivation(s) for pubkey in pubkeys: sk = subkeys[pubkey] write_kv(PSBT_OUT_BIP32_DERIVATION, sk, pubkey) out_fd.write(b'\x00') # capture for later use tx.raw_psbt = out_fd.getvalue() return tx.raw_psbt