示例#1
0
    def sign_tx(self, tx):
        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 = proto.TxInputType()

                # Set the input stuff
                txinputtype.prev_hash = ser_uint256(txin.prevout.hash)[::-1]
                txinputtype.prev_index = txin.prevout.n
                txinputtype.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 = proto.InputScriptType.SPENDP2SHWITNESS
                    else:
                        txinputtype.script_type = proto.InputScriptType.SPENDWITNESS
                else:
                    txinputtype.script_type = proto.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():
                    txinputtype.address_n = [
                        0x80000000 | 84,
                        0x80000000 | (1 if self.is_testnet else 0),
                    ]
                    txinputtype.multisig = None
                    txinputtype.script_type = proto.InputScriptType.SPENDWITNESS
                    inputs.append(txinputtype)
                    to_ignore.append(input_num)

                # Check for multisig
                is_ms, multisig = parse_multisig(scriptcode)
                if is_ms:
                    txinputtype.multisig = parse_multisig_xpubs(
                        tx, psbt_in, multisig)
                    if not is_wit:
                        if utxo.is_p2sh:
                            txinputtype.script_type = (
                                proto.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[0] == 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[1:]
                            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.is_testnet:
                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 = proto.TxOutputType()
                txoutput.amount = out.nValue
                txoutput.script_type = proto.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)
                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[0] == master_fp:
                        wit, ver, prog = out.is_witness()
                        if out.is_p2pkh():
                            txoutput.address_n = keypath[1:]
                            txoutput.address = None
                        elif wit:
                            txoutput.script_type = proto.OutputScriptType.PAYTOWITNESS
                            txoutput.address_n = keypath[1:]
                            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) == 20:
                                txoutput.script_type = (
                                    proto.OutputScriptType.PAYTOP2SHWITNESS)
                                txoutput.address_n = keypath[1:]
                                txoutput.address = None
                        is_ms, multisig = parse_multisig(
                            psbt_out.witness_script if wit else psbt_out.
                            redeem_script)
                        if is_ms:
                            txoutput.multisig = parse_multisig_xpubs(
                                tx, psbt_out, 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 = proto.TransactionType()
                    t.version = prev.nVersion
                    t.lock_time = prev.nLockTime

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

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

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

            # 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][0]
                    if fp == master_fp and pubkey not in psbt_in.partial_sigs:
                        psbt_in.partial_sigs[pubkey] = sig + b"\x01"
                        break

            p += 1

        return {"psbt": tx.serialize()}
示例#2
0
    def sign_tx(self, tx):
        c_tx = CTransaction(tx.tx)
        tx_bytes = c_tx.serialize_with_witness()

        # Master key fingerprint
        master_fpr = hash160(
            compress_public_key(
                self.app.getWalletPublicKey("")["publicKey"]))[:4]
        # An entry per input, each with 0 to many keys to sign with
        all_signature_attempts = [[]] * len(c_tx.vin)

        # Get the app version to determine whether to use Trusted Input for segwit
        version = self.app.getFirmwareVersion()
        use_trusted_segwit = (
            version["major_version"] == 1
            and version["minor_version"] >= 4) or version["major_version"] > 1

        # NOTE: We only support signing Segwit inputs, where we can skip over non-segwit
        # inputs, or non-segwit inputs, where *all* inputs are non-segwit. This is due
        # to Ledger's mutually exclusive signing steps for each type.
        segwit_inputs = []
        # Legacy style inputs
        legacy_inputs = []

        has_segwit = False
        has_legacy = False

        script_codes = [[]] * len(c_tx.vin)

        # Detect changepath, (p2sh-)p2(w)pkh only
        change_path = ""
        for txout, i_num in zip(c_tx.vout, range(len(c_tx.vout))):
            # Find which wallet key could be change based on hdsplit: m/.../1/k
            # Wallets shouldn't be sending to change address as user action
            # otherwise this will get confused
            for pubkey, path in tx.outputs[i_num].hd_keypaths.items():
                if (struct.pack("<I", path[0]) == master_fpr and len(path) > 2
                        and path[-2] == 1):
                    # For possible matches, check if pubkey matches possible template
                    if (hash160(pubkey) in txout.scriptPubKey or hash160(
                            bytearray.fromhex("0014") + hash160(pubkey))
                            in txout.scriptPubKey):
                        change_path = ""
                        for index in path[1:]:
                            change_path += str(index) + "/"
                        change_path = change_path[:-1]

        for txin, psbt_in, i_num in zip(c_tx.vin, tx.inputs,
                                        range(len(c_tx.vin))):

            seq = format(txin.nSequence, "x")
            seq = seq.zfill(8)
            seq = bytearray.fromhex(seq)
            seq.reverse()
            seq_hex = "".join("{:02x}".format(x) for x in seq)

            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(i_num))
                utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n]
            if utxo is None:
                raise Exception(
                    "PSBT is missing input utxo information, cannot sign")
            scriptcode = utxo.scriptPubKey

            if is_p2sh(scriptcode):
                if len(psbt_in.redeem_script) == 0:
                    continue
                scriptcode = psbt_in.redeem_script

            is_wit, _, _ = is_witness(scriptcode)

            segwit_inputs.append({
                "value":
                txin.prevout.serialize() + struct.pack("<Q", utxo.nValue),
                "witness":
                True,
                "sequence":
                seq_hex,
            })
            if is_wit:
                if is_p2wsh(scriptcode):
                    if len(psbt_in.witness_script) == 0:
                        continue
                    scriptcode = psbt_in.witness_script
                elif is_p2wpkh(scriptcode):
                    _, _, wit_prog = is_witness(scriptcode)
                    scriptcode = b"\x76\xa9\x14" + wit_prog + b"\x88\xac"
                else:
                    continue
                has_segwit = True
            else:
                # We only need legacy inputs in the case where all inputs are legacy, we check
                # later
                ledger_prevtx = bitcoinTransaction(
                    psbt_in.non_witness_utxo.serialize())
                legacy_inputs.append(
                    self.app.getTrustedInput(ledger_prevtx, txin.prevout.n))
                legacy_inputs[-1]["sequence"] = seq_hex
                has_legacy = True

            if psbt_in.non_witness_utxo and use_trusted_segwit:
                ledger_prevtx = bitcoinTransaction(
                    psbt_in.non_witness_utxo.serialize())
                segwit_inputs[-1].update(
                    self.app.getTrustedInput(ledger_prevtx, txin.prevout.n))

            pubkeys = []
            signature_attempts = []

            # Save scriptcode for later signing
            script_codes[i_num] = scriptcode

            # Find which pubkeys could sign this input (should be all?)
            for pubkey in psbt_in.hd_keypaths.keys():
                if hash160(pubkey) in scriptcode or pubkey in scriptcode:
                    pubkeys.append(pubkey)

            # Figure out which keys in inputs are from our wallet
            for pubkey in pubkeys:
                keypath = psbt_in.hd_keypaths[pubkey]
                if master_fpr == struct.pack("<I", keypath[0]):
                    # Add the keypath strings
                    keypath_str = ""
                    for index in keypath[1:]:
                        keypath_str += str(index) + "/"
                    keypath_str = keypath_str[:-1]
                    signature_attempts.append([keypath_str, pubkey])

            all_signature_attempts[i_num] = signature_attempts

        # Sign any segwit inputs
        if has_segwit:
            # Process them up front with all scriptcodes blank
            blank_script_code = bytearray()
            for i in range(len(segwit_inputs)):
                self.app.startUntrustedTransaction(
                    i == 0,
                    i,
                    segwit_inputs,
                    script_codes[i]
                    if use_trusted_segwit else blank_script_code,
                    c_tx.nVersion,
                )

            # Number of unused fields for Nano S, only changepath and transaction in bytes req
            self.app.finalizeInput(b"DUMMY", -1, -1, change_path, tx_bytes)

            # For each input we control do segwit signature
            for i in range(len(segwit_inputs)):
                for signature_attempt in all_signature_attempts[i]:
                    self.app.startUntrustedTransaction(False, 0,
                                                       [segwit_inputs[i]],
                                                       script_codes[i],
                                                       c_tx.nVersion)
                    tx.inputs[i].partial_sigs[
                        signature_attempt[1]] = self.app.untrustedHashSign(
                            signature_attempt[0], "", c_tx.nLockTime, 0x01)
        elif has_legacy:
            first_input = True
            # Legacy signing if all inputs are legacy
            for i in range(len(legacy_inputs)):
                for signature_attempt in all_signature_attempts[i]:
                    assert tx.inputs[i].non_witness_utxo is not None
                    self.app.startUntrustedTransaction(first_input, i,
                                                       legacy_inputs,
                                                       script_codes[i],
                                                       c_tx.nVersion)
                    self.app.finalizeInput(b"DUMMY", -1, -1, change_path,
                                           tx_bytes)
                    tx.inputs[i].partial_sigs[
                        signature_attempt[1]] = self.app.untrustedHashSign(
                            signature_attempt[0], "", c_tx.nLockTime, 0x01)
                    first_input = False

        # Send PSBT back
        return {"psbt": tx.serialize()}