예제 #1
0
    def sign_tx(self, tx: PSBT) -> PSBT:
        """
        Sign a transaction with the Blockstream Jade.
        """
        # Helper to get multisig record for change output
        def _parse_signers(
            hd_keypath_origins: List[KeyOriginInfo],
        ) -> Tuple[List[Tuple[bytes, Sequence[int]]], List[Sequence[int]]]:
            # Split the path at the last hardened path element
            def _split_at_last_hardened_element(
                path: Sequence[int],
            ) -> Tuple[Sequence[int], Sequence[int]]:
                for i in range(len(path), 0, -1):
                    if is_hardened(path[i - 1]):
                        return (path[:i], path[i:])
                return ([], path)

            signers = []
            paths = []
            for origin in hd_keypath_origins:
                prefix, suffix = _split_at_last_hardened_element(origin.path)
                signers.append((origin.fingerprint, prefix))
                paths.append(suffix)
            # sort signers and paths like in multisig registration
            signers, paths = [list(a) for a in zip(*sorted(zip(signers, paths)))]

            return signers, paths

        c_txn = CTransaction(tx.tx)
        master_fp = self.get_master_fingerprint()
        signing_singlesigs = False
        signing_multisigs = {}
        need_to_sign = True

        while need_to_sign:
            signing_pubkeys: List[Optional[bytes]] = [None] * len(tx.inputs)
            need_to_sign = False

            # Signing input details
            jade_inputs = []
            for n_vin, (txin, psbtin) in py_enumerate(zip(c_txn.vin, tx.inputs)):
                # Get bip32 path to use to sign, if required for this input
                path = None
                multisig_input = len(psbtin.hd_keypaths) > 1
                for pubkey, origin in psbtin.hd_keypaths.items():
                    if origin.fingerprint == master_fp and len(origin.path) > 0:
                        if not multisig_input:
                            signing_singlesigs = True

                        if psbtin.partial_sigs.get(pubkey, None) is None:
                            # hw to sign this input - it is not already signed
                            if signing_pubkeys[n_vin] is None:
                                signing_pubkeys[n_vin] = pubkey
                                path = origin.path
                            else:
                                # Additional signature needed for this input - ie. a multisig where this wallet is
                                # multiple signers?  Clumsy, but just loop and go through the signing procedure again.
                                need_to_sign = True

                # Get the tx and prevout/scriptcode
                utxo = None
                p2sh = False
                input_txn_bytes = None
                if psbtin.witness_utxo:
                    utxo = psbtin.witness_utxo
                if psbtin.non_witness_utxo:
                    utxo = psbtin.non_witness_utxo.vout[txin.prevout.n]
                    input_txn_bytes = (
                        psbtin.non_witness_utxo.serialize_without_witness()
                    )
                if utxo is None:
                    raise Exception(
                        "PSBT is missing input utxo information, cannot sign"
                    )
                scriptcode = utxo.scriptPubKey

                if is_p2sh(scriptcode):
                    scriptcode = psbtin.redeem_script
                    p2sh = True

                witness_input, witness_version, witness_program = is_witness(scriptcode)

                if witness_input:
                    if is_p2wsh(scriptcode):
                        scriptcode = psbtin.witness_script
                    elif is_p2wpkh(scriptcode):
                        scriptcode = b"\x76\xa9\x14" + witness_program + b"\x88\xac"
                    else:
                        continue

                # If we are signing a multisig input, deduce the potential
                # registration details and cache as a potential change wallet
                if multisig_input and path and scriptcode and (p2sh or witness_input):
                    parsed = parse_multisig(scriptcode)
                    if parsed:
                        addr_type = (
                            AddressType.LEGACY
                            if not witness_input
                            else AddressType.WIT
                            if not p2sh
                            else AddressType.SH_WIT
                        )
                        script_variant = self._convertAddrType(addr_type, multisig=True)
                        threshold = parsed[0]

                        pubkeys = parsed[1]
                        hd_keypath_origins = [
                            psbtin.hd_keypaths[pubkey] for pubkey in pubkeys
                        ]

                        signers, paths = _parse_signers(hd_keypath_origins)

                        multisig_name = self._get_multisig_name(
                            script_variant, threshold, signers
                        )
                        signing_multisigs[multisig_name] = (
                            script_variant,
                            threshold,
                            signers,
                        )

                # Build the input and add to the list - include some host entropy for AE sigs (although we won't verify)
                jade_inputs.append(
                    {
                        "is_witness": witness_input,
                        "input_tx": input_txn_bytes,
                        "script": scriptcode,
                        "path": path,
                        "ae_host_entropy": os.urandom(32),
                        "ae_host_commitment": os.urandom(32),
                    }
                )

            # Change output details
            # This is optional, in that if we send it Jade validates the change output script
            # and the user need not confirm that ouptut.  If not passed the change output must
            # be confirmed by the user on the hwwallet screen, like any other spend output.
            change: List[Optional[Dict[str, Any]]] = [None] * len(tx.outputs)

            # If signing multisig inputs, get registered multisigs details in case we
            # see any multisig outputs which may be change which we can auto-validate.
            # ie. filter speculative 'signing multisigs' to ones actually registered on the hw
            candidate_multisigs = {}

            if signing_multisigs:
                # register multisig if xpubs are known
                if tx.xpub and len(signing_multisigs) == 1:
                    msigname = list(signing_multisigs.keys())[0]
                    signers = []
                    origins = []
                    for xpub in tx.xpub:
                        hd = bip32.HDKey.parse(xpub)
                        origin = tx.xpub[xpub]
                        origins.append((origin.fingerprint, origin.path))

                        signers.append(
                            {
                                "fingerprint": origin.fingerprint,
                                "derivation": origin.path,
                                "xpub": str(hd),
                                "path": [],
                            }
                        )

                    # sort origins and signers together
                    origins, signers = [
                        list(a) for a in zip(*sorted(zip(origins, signers)))
                    ]

                    # Get a deterministic name for this multisig wallet
                    script_variant = signing_multisigs[msigname][0]
                    thresh = signing_multisigs[msigname][1]
                    num_signers = signing_multisigs[msigname][2]
                    multisig_name = self._get_multisig_name(
                        script_variant, thresh, origins
                    )
                    # stupid sanity check of the fingerprints and origins
                    if multisig_name == msigname:
                        # Need to ensure this multisig wallet is registered first
                        # (Note: 're-registering' is a no-op)
                        self.jade.register_multisig(
                            self._network(),
                            multisig_name,
                            script_variant,
                            True,  # always use sorted
                            thresh,
                            signers,
                        )
                #
                registered_multisigs = self.jade.get_registered_multisigs()
                signing_multisigs = {
                    k: v
                    for k, v in signing_multisigs.items()
                    if k in registered_multisigs
                    and registered_multisigs[k]["variant"] == v[0]
                    and registered_multisigs[k]["threshold"] == v[1]
                    and registered_multisigs[k]["num_signers"] == len(v[2])
                }

            # Look at every output...
            for n_vout, (txout, psbtout) in py_enumerate(zip(c_txn.vout, tx.outputs)):
                num_signers = len(psbtout.hd_keypaths)

                if num_signers == 1 and signing_singlesigs:
                    # Single-sig output - since we signed singlesig inputs this could be our change
                    for pubkey, origin in psbtout.hd_keypaths.items():
                        # Considers 'our' outputs as potential change as far as Jade is concerned
                        # ie. can be verified and auto-confirmed.
                        # Is this ok, or should check path also, assuming bip44-like ?
                        if origin.fingerprint == master_fp and len(origin.path) > 0:
                            change_addr_type = None
                            if txout.is_p2pkh():
                                change_addr_type = AddressType.LEGACY
                            elif txout.is_witness()[0] and not txout.is_p2wsh():
                                change_addr_type = AddressType.WIT  # ie. p2wpkh
                            elif (
                                txout.is_p2sh() and is_witness(psbtout.redeem_script)[0]
                            ):
                                change_addr_type = AddressType.SH_WIT
                            else:
                                continue

                            script_variant = self._convertAddrType(
                                change_addr_type, multisig=False
                            )
                            change[n_vout] = {
                                "path": origin.path,
                                "variant": script_variant,
                            }

                elif num_signers > 1 and signing_multisigs:
                    # Multisig output - since we signed multisig inputs this could be our change
                    candidate_multisigs = {
                        k: v
                        for k, v in signing_multisigs.items()
                        if len(v[2]) == num_signers
                    }
                    if not candidate_multisigs:
                        continue

                    for pubkey, origin in psbtout.hd_keypaths.items():
                        if origin.fingerprint == master_fp and len(origin.path) > 0:
                            change_addr_type = None
                            if (
                                txout.is_p2sh()
                                and not is_witness(psbtout.redeem_script)[0]
                            ):
                                change_addr_type = AddressType.LEGACY
                                scriptcode = psbtout.redeem_script
                            elif txout.is_p2wsh() and not txout.is_p2sh():
                                change_addr_type = AddressType.WIT
                                scriptcode = psbtout.witness_script
                            elif (
                                txout.is_p2sh() and is_witness(psbtout.redeem_script)[0]
                            ):
                                change_addr_type = AddressType.SH_WIT
                                scriptcode = psbtout.witness_script
                            else:
                                continue

                            parsed = parse_multisig(scriptcode)
                            if parsed:
                                script_variant = self._convertAddrType(
                                    change_addr_type, multisig=True
                                )
                                threshold = parsed[0]

                                pubkeys = parsed[1]
                                hd_keypath_origins = [
                                    psbtout.hd_keypaths[pubkey] for pubkey in pubkeys
                                ]

                                signers, paths = _parse_signers(hd_keypath_origins)

                                multisig_name = self._get_multisig_name(
                                    script_variant, threshold, signers
                                )

                                matched_multisig = candidate_multisigs.get(
                                    multisig_name
                                ) == (script_variant, threshold, signers)
                                if matched_multisig:
                                    change[n_vout] = {
                                        "paths": paths,
                                        "multisig_name": multisig_name,
                                    }

            # The txn itself
            txn_bytes = c_txn.serialize_without_witness()

            # Request Jade generate the signatures for our inputs.
            # Change details are passed to be validated on the hw (user does not confirm)
            signatures = self.jade.sign_tx(
                self._network(), txn_bytes, jade_inputs, change, True
            )

            # Push sigs into PSBT structure as appropriate
            for psbtin, signer_pubkey, sigdata in zip(
                tx.inputs, signing_pubkeys, signatures
            ):
                signer_commitment, sig = sigdata
                if signer_pubkey and sig:
                    psbtin.partial_sigs[signer_pubkey] = sig

        # Return the updated psbt
        return tx
예제 #2
0
    def sign_tx(self, tx: PSBT) -> PSBT:
        """
        Sign a transaction with the Trezor. There are some limitations to what transactions can be signed.

        - Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation.
        - Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation.
        - Send-to-self transactions will result in no prompt for outputs as all outputs will be detected as change.
        """
        self._check_unlocked()

        # Get this devices master key fingerprint
        master_key = btc.get_public_node(self.client, [0x80000000], coin_name="Bitcoin")
        master_fp = get_xpub_fingerprint(master_key.xpub)

        # Do multiple passes for multisig
        passes = 1
        p = 0

        while p < passes:
            # Prepare inputs
            inputs = []
            to_ignore = (
                []
            )  # Note down which inputs whose signatures we're going to ignore
            for input_num, (psbt_in, txin) in py_enumerate(
                list(zip(tx.inputs, tx.tx.vin))
            ):
                txinputtype = messages.TxInputType(
                    prev_hash=ser_uint256(txin.prevout.hash)[::-1],
                    prev_index=txin.prevout.n,
                    sequence=txin.nSequence,
                )

                # Detrermine spend type
                scriptcode = b""
                utxo = None
                if psbt_in.witness_utxo:
                    utxo = psbt_in.witness_utxo
                if psbt_in.non_witness_utxo:
                    if txin.prevout.hash != psbt_in.non_witness_utxo.sha256:
                        raise BadArgumentError(
                            "Input {} has a non_witness_utxo with the wrong hash".format(
                                input_num
                            )
                        )
                    utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n]
                if utxo is None:
                    continue
                scriptcode = utxo.scriptPubKey

                # Check if P2SH
                p2sh = False
                if is_p2sh(scriptcode):
                    # Look up redeemscript
                    if len(psbt_in.redeem_script) == 0:
                        continue
                    scriptcode = psbt_in.redeem_script
                    p2sh = True

                # Check segwit
                is_wit, _, _ = is_witness(scriptcode)

                if is_wit:
                    if p2sh:
                        txinputtype.script_type = (
                            messages.InputScriptType.SPENDP2SHWITNESS
                        )
                    else:
                        txinputtype.script_type = messages.InputScriptType.SPENDWITNESS
                else:
                    txinputtype.script_type = messages.InputScriptType.SPENDADDRESS
                txinputtype.amount = utxo.nValue

                # Check if P2WSH
                p2wsh = False
                if is_p2wsh(scriptcode):
                    # Look up witnessscript
                    if len(psbt_in.witness_script) == 0:
                        continue
                    scriptcode = psbt_in.witness_script
                    p2wsh = True

                def ignore_input() -> None:
                    txinputtype.address_n = [
                        0x80000000 | 84,
                        0x80000000 | (0 if self.chain == Chain.MAIN else 1),
                        0x80000000,
                        0,
                        0,
                    ]
                    txinputtype.multisig = None
                    txinputtype.script_type = messages.InputScriptType.SPENDWITNESS
                    inputs.append(txinputtype)
                    to_ignore.append(input_num)

                # Check for multisig
                is_ms, multisig = parse_multisig(scriptcode, tx.xpub, psbt_in)
                if is_ms:
                    # Add to txinputtype
                    txinputtype.multisig = multisig
                    if not is_wit:
                        if utxo.is_p2sh:
                            txinputtype.script_type = (
                                messages.InputScriptType.SPENDMULTISIG
                            )
                        else:
                            # Cannot sign bare multisig, ignore it
                            ignore_input()
                            continue
                elif not is_ms and not is_wit and not is_p2pkh(scriptcode):
                    # Cannot sign unknown spk, ignore it
                    ignore_input()
                    continue
                elif not is_ms and is_wit and p2wsh:
                    # Cannot sign unknown witness script, ignore it
                    ignore_input()
                    continue

                # Find key to sign with
                found = False  # Whether we have found a key to sign with
                found_in_sigs = (
                    False  # Whether we have found one of our keys in the signatures
                )
                our_keys = 0
                for key in psbt_in.hd_keypaths.keys():
                    keypath = psbt_in.hd_keypaths[key]
                    if keypath.fingerprint == master_fp:
                        if (
                            key in psbt_in.partial_sigs
                        ):  # This key already has a signature
                            found_in_sigs = True
                            continue
                        if (
                            not found
                        ):  # This key does not have a signature and we don't have a key to sign with yet
                            txinputtype.address_n = keypath.path
                            found = True
                        our_keys += 1

                # Determine if we need to do more passes to sign everything
                if our_keys > passes:
                    passes = our_keys

                if (
                    not found and not found_in_sigs
                ):  # None of our keys were in hd_keypaths or in partial_sigs
                    # This input is not one of ours
                    ignore_input()
                    continue
                elif (
                    not found and found_in_sigs
                ):  # All of our keys are in partial_sigs, ignore whatever signature is produced for this input
                    ignore_input()
                    continue

                # append to inputs
                inputs.append(txinputtype)

            # address version byte
            if self.chain != Chain.MAIN:
                p2pkh_version = b"\x6f"
                p2sh_version = b"\xc4"
                bech32_hrp = "tb"
            else:
                p2pkh_version = b"\x00"
                p2sh_version = b"\x05"
                bech32_hrp = "bc"

            # prepare outputs
            outputs = []
            for i, out in py_enumerate(tx.tx.vout):
                txoutput = messages.TxOutputType(amount=out.nValue)
                txoutput.script_type = messages.OutputScriptType.PAYTOADDRESS
                if out.is_p2pkh():
                    txoutput.address = to_address(out.scriptPubKey[3:23], p2pkh_version)
                elif out.is_p2sh():
                    txoutput.address = to_address(out.scriptPubKey[2:22], p2sh_version)
                elif out.is_opreturn():
                    txoutput.script_type = messages.OutputScriptType.PAYTOOPRETURN
                    txoutput.op_return_data = out.scriptPubKey[2:]
                else:
                    wit, ver, prog = out.is_witness()
                    if wit:
                        txoutput.address = bech32.encode(bech32_hrp, ver, prog)
                    else:
                        raise BadArgumentError("Output is not an address")

                # Add the derivation path for change
                psbt_out = tx.outputs[i]
                for _, keypath in psbt_out.hd_keypaths.items():
                    if keypath.fingerprint != master_fp:
                        continue
                    wit, ver, prog = out.is_witness()
                    if out.is_p2pkh():
                        txoutput.address_n = keypath.path
                        txoutput.address = None
                    elif wit:
                        txoutput.script_type = messages.OutputScriptType.PAYTOWITNESS
                        txoutput.address_n = keypath.path
                        txoutput.address = None
                    elif out.is_p2sh() and psbt_out.redeem_script:
                        wit, ver, prog = CTxOut(0, psbt_out.redeem_script).is_witness()
                        if wit and len(prog) in [20, 32]:
                            txoutput.script_type = (
                                messages.OutputScriptType.PAYTOP2SHWITNESS
                            )
                            txoutput.address_n = keypath.path
                            txoutput.address = None

                # add multisig info
                is_ms, multisig = parse_multisig(
                    psbt_out.witness_script or psbt_out.redeem_script, tx.xpub, psbt_out
                )
                if is_ms:
                    txoutput.multisig = multisig

                # append to outputs
                outputs.append(txoutput)

            # Prepare prev txs
            prevtxs = {}
            for psbt_in in tx.inputs:
                if psbt_in.non_witness_utxo:
                    prev = psbt_in.non_witness_utxo

                    t = messages.TransactionType()
                    t.version = prev.nVersion
                    t.lock_time = prev.nLockTime

                    for vin in prev.vin:
                        i = messages.TxInputType(
                            prev_hash=ser_uint256(vin.prevout.hash)[::-1],
                            prev_index=vin.prevout.n,
                            script_sig=vin.scriptSig,
                            sequence=vin.nSequence,
                        )
                        t.inputs.append(i)

                    for vout in prev.vout:
                        o = messages.TxOutputBinType(
                            amount=vout.nValue,
                            script_pubkey=vout.scriptPubKey,
                        )
                        t.bin_outputs.append(o)
                    logging.debug(psbt_in.non_witness_utxo.hash)
                    assert psbt_in.non_witness_utxo.sha256 is not None
                    prevtxs[ser_uint256(psbt_in.non_witness_utxo.sha256)[::-1]] = t

            # Sign the transaction
            signed_tx = btc.sign_tx(
                client=self.client,
                coin_name=self.coin_name,
                inputs=inputs,
                outputs=outputs,
                prev_txes=prevtxs,
                version=tx.tx.nVersion,
                lock_time=tx.tx.nLockTime,
            )

            # Each input has one signature
            for input_num, (psbt_in, sig) in py_enumerate(
                list(zip(tx.inputs, signed_tx[0]))
            ):
                if input_num in to_ignore:
                    continue
                for pubkey in psbt_in.hd_keypaths.keys():
                    fp = psbt_in.hd_keypaths[pubkey].fingerprint
                    if fp == master_fp and pubkey not in psbt_in.partial_sigs:
                        psbt_in.partial_sigs[pubkey] = sig + b"\x01"
                        break

            p += 1

        return tx
예제 #3
0
    def sign_tx(self, tx: PSBT) -> PSBT:
        """
        Sign a transaction with the Blockstream Jade.

        - Jade can only be used to sign single-key inputs at this time.   It cannot sign multisig or arbitrary scripts.
        """

        c_txn = CTransaction(tx.tx)
        master_fp = self.get_master_fingerprint()
        signing_pubkeys = [None] * len(tx.inputs)

        # Signing input details
        jade_inputs = []
        for n_vin, (txin, psbtin) in py_enumerate(zip(c_txn.vin, tx.inputs)):
            # Get bip32 path to use to sign, if required for this input
            path = None
            for pubkey, origin in psbtin.hd_keypaths.items():
                if origin.fingerprint == master_fp and len(origin.path) > 0:
                    # Our input
                    if (pubkey not in psbtin.partial_sigs
                            or not psbtin.partial_sigs[pubkey]):
                        # hw to sign this input - it is not already signed
                        signing_pubkeys[n_vin] = pubkey
                        path = origin.path

            # Get the tx and prevout/scriptcode
            utxo = None
            input_txn_bytes = None
            if psbtin.witness_utxo:
                utxo = psbtin.witness_utxo
            if psbtin.non_witness_utxo:
                if txin.prevout.hash != psbtin.non_witness_utxo.sha256:
                    raise BadArgumentError(
                        "Input {} has a non_witness_utxo with the wrong hash".
                        format(n_vin))
                utxo = psbtin.non_witness_utxo.vout[txin.prevout.n]
                input_txn_bytes = psbtin.non_witness_utxo.serialize_without_witness(
                )

            scriptcode = utxo.scriptPubKey

            if is_p2sh(scriptcode):
                scriptcode = psbtin.redeem_script

            witness_input, witness_version, witness_program = is_witness(
                scriptcode)

            if witness_input:
                if is_p2wsh(scriptcode):
                    scriptcode = psbtin.witness_script
                elif is_p2wpkh(scriptcode):
                    scriptcode = b"\x76\xa9\x14" + witness_program + b"\x88\xac"
                else:
                    scriptcode = None

            # Build the input and add to the list
            jade_inputs.append({
                "is_witness": witness_input,
                "input_tx": input_txn_bytes,
                "script": scriptcode,
                "path": path,
            })

        # Change output details
        # This is optional, in that if we send it Jade validates the change output script
        # and the user need not confirm that ouptut.  If not passed the change output must
        # be confirmed by the user on the hwwallet screen, like any other spend output.
        change = [None] * len(tx.outputs)
        for n_vout, (txout,
                     psbtout) in py_enumerate(zip(c_txn.vout, tx.outputs)):
            for pubkey, origin in psbtout.hd_keypaths.items():
                # Considers 'our' outputs as change as far as Jade is concerned
                # ie. can be auto-confirmed.
                # Is this ok, or should check path also, assuming bip44-like ?
                if origin.fingerprint == master_fp and len(origin.path) > 0:
                    addr_type = None
                    if txout.is_p2pkh():
                        addr_type = AddressType.LEGACY
                    elif txout.is_witness()[0] and not txout.is_p2wsh():
                        addr_type = AddressType.WIT
                    elif txout.is_p2sh():
                        addr_type = AddressType.SH_WIT  # is it really though ?

                    if addr_type:
                        addr_type = self._convertAddrType(addr_type)
                        change[n_vout] = {
                            "path": origin.path,
                            "variant": addr_type
                        }

        # The txn itself
        txn_bytes = c_txn.serialize_without_witness()

        # Request Jade generate the signatures for our inputs.
        # Change details are passed to be validated on the hw (user does not confirm)
        signatures = self.jade.sign_tx(self._network(), txn_bytes, jade_inputs,
                                       change)

        # Push sigs into PSBT structure as appropriate
        for psbtin, pubkey, sig in zip(tx.inputs, signing_pubkeys, signatures):
            if pubkey and sig:
                psbtin.partial_sigs[pubkey] = sig

        # Return the updated psbt
        return tx