Example #1
0
    def _generate_and_finalize(self, unknown_inputs, psbt):
        if not unknown_inputs:
            # Just do the normal signing process to test "all inputs" case
            sign_res = self.do_command(self.dev_args +
                                       ['signtx', psbt['psbt']])
            finalize_res = self.wrpc.finalizepsbt(sign_res['psbt'])
        else:
            # Sign only input one on first pass
            # then rest on second pass to test ability to successfully
            # ignore inputs that are not its own. Then combine both
            # signing passes to ensure they are actually properly being
            # partially signed at each step.
            first_psbt = PSBT()
            first_psbt.deserialize(psbt['psbt'])
            second_psbt = PSBT()
            second_psbt.deserialize(psbt['psbt'])

            # Blank master fingerprint to make hww fail to sign
            # Single input PSBTs will be fully signed by first signer
            for psbt_input in first_psbt.inputs[1:]:
                for pubkey, path in psbt_input.hd_keypaths.items():
                    psbt_input.hd_keypaths[pubkey] = KeyOriginInfo(
                        b"\x00\x00\x00\x00", path.path)
            for pubkey, path in second_psbt.inputs[0].hd_keypaths.items():
                second_psbt.inputs[0].hd_keypaths[pubkey] = KeyOriginInfo(
                    b"\x00\x00\x00\x00", path.path)

            single_input = len(first_psbt.inputs) == 1

            # Process the psbts
            first_psbt = first_psbt.serialize()
            second_psbt = second_psbt.serialize()

            # First will always have something to sign
            first_sign_res = self.do_command(self.dev_args +
                                             ['signtx', first_psbt])
            self.assertTrue(single_input == self.wrpc.finalizepsbt(
                first_sign_res['psbt'])['complete'])
            # Second may have nothing to sign (1 input case)
            # and also may throw an error(e.g., ColdCard)
            second_sign_res = self.do_command(self.dev_args +
                                              ['signtx', second_psbt])
            if 'psbt' in second_sign_res:
                self.assertTrue(not self.wrpc.finalizepsbt(
                    second_sign_res['psbt'])['complete'])
                combined_psbt = self.wrpc.combinepsbt(
                    [first_sign_res['psbt'], second_sign_res['psbt']])

            else:
                self.assertTrue('error' in second_sign_res)
                combined_psbt = first_sign_res['psbt']

            finalize_res = self.wrpc.finalizepsbt(combined_psbt)
            self.assertTrue(finalize_res['complete'])
            self.assertTrue(
                self.wrpc.testmempoolaccept([finalize_res['hex']
                                             ])[0]["allowed"])
        return finalize_res['hex']
Example #2
0
    def sign_tx(self, psbt: PSBT) -> Dict[str, str]:
        """Sign a partially signed bitcoin transaction (PSBT).

        Return {"psbt": <base64 psbt string>}.
        """
        # this one can hang for quite some time
        response = self.query("sign %s" % psbt.serialize())
        signed_psbt = PSBT()
        signed_psbt.deserialize(response)
        # adding partial sigs to initial tx
        for i in range(len(psbt.inputs)):
            for k in signed_psbt.inputs[i].partial_sigs:
                psbt.inputs[i].partial_sigs[k] = signed_psbt.inputs[
                    i].partial_sigs[k]
        return {'psbt': psbt.serialize()}
Example #3
0
def clean_psbt(b64psbt):
    psbt = PSBT()
    psbt.deserialize(b64psbt)
    for inp in psbt.inputs:
        if inp.witness_utxo is not None and inp.non_witness_utxo is not None:
            inp.non_witness_utxo = None
    return psbt.serialize()
Example #4
0
 def test_valid_psbt(self):
     for valid in self.data['valid']:
         with self.subTest(valid=valid):
             psbt = PSBT()
             psbt.deserialize(valid)
             serd = psbt.serialize()
             self.assertEqual(valid, serd)
Example #5
0
 def fill_psbt(self, b64psbt, non_witness: bool = True, xpubs: bool = True):
     psbt = PSBT()
     psbt.deserialize(b64psbt)
     if non_witness:
         for i, inp in enumerate(psbt.tx.vin):
             txid = inp.prevout.hash.to_bytes(32, 'big').hex()
             try:
                 res = self.cli.gettransaction(txid)
             except:
                 raise SpecterError(
                     "Can't find previous transaction in the wallet.")
             stream = BytesIO(bytes.fromhex(res["hex"]))
             prevtx = CTransaction()
             prevtx.deserialize(stream)
             psbt.inputs[i].non_witness_utxo = prevtx
     if xpubs:
         # for multisig add xpub fields
         if len(self.keys) > 1:
             for k in self.keys:
                 key = b'\x01' + decode_base58(k.xpub)
                 if k.fingerprint != '':
                     fingerprint = bytes.fromhex(k.fingerprint)
                 else:
                     fingerprint = get_xpub_fingerprint(k.xpub)
                 if k.derivation != '':
                     der = der_to_bytes(k.derivation)
                 else:
                     der = b''
                 value = fingerprint + der
                 psbt.unknown[key] = value
     return psbt.serialize()
Example #6
0
    def sign_tx(self, psbt: PSBT) -> Dict[str, str]:
        """Sign a partially signed bitcoin transaction (PSBT).

        Return {"psbt": <base64 psbt string>}.
        """
        # this one can hang for quite some time
        signed_tx = self.query("sign %s" % psbt.serialize())
        return {"psbt": signed_tx}
Example #7
0
 def fill_psbt(self, b64psbt):
     psbt = PSBT()
     psbt.deserialize(b64psbt)
     for i, inp in enumerate(psbt.tx.vin):
         txid = inp.prevout.hash.to_bytes(32,'big').hex()
         try:
             res = self.cli.gettransaction(txid)
         except:
             raise SpecterError("Can't find previous transaction in the wallet.")
         stream = BytesIO(bytes.fromhex(res["hex"]))
         prevtx = CTransaction()
         prevtx.deserialize(stream)
         psbt.inputs[i].non_witness_utxo = prevtx
     return psbt.serialize()
Example #8
0
 def create_psbts(self, base64_psbt, wallet):
     psbts = HWIDevice.create_psbts(self, base64_psbt, wallet)
     sdcard_psbt = PSBT()
     sdcard_psbt.deserialize(base64_psbt)
     if len(wallet.keys) > 1:
         for k in wallet.keys:
             key = b'\x01' + decode_base58(k.xpub)
             if k.fingerprint != '':
                 fingerprint = bytes.fromhex(k.fingerprint)
             else:
                 fingerprint = _get_xpub_fingerprint(k.xpub)
             if k.derivation != '':
                 der = _der_to_bytes(k.derivation)
             else:
                 der = b''
             value = fingerprint + der
             sdcard_psbt.unknown[key] = value
     psbts['sdcard'] = sdcard_psbt.serialize()
     return psbts
Example #9
0
 def create_psbts(self, base64_psbt, wallet):
     psbts = super().create_psbts(base64_psbt, wallet)
     qr_psbt = PSBT()
     qr_psbt.deserialize(base64_psbt)
     for inp in qr_psbt.inputs + qr_psbt.outputs:
         inp.witness_script = b""
         inp.redeem_script = b""
         if len(inp.hd_keypaths) > 0:
             k = list(inp.hd_keypaths.keys())[0]
             # proprietary field - wallet derivation path
             # only contains two last derivation indexes - change and index
             inp.unknown[b"\xfc\xca\x01" +
                         get_wallet_fingerprint(wallet)] = b"".join([
                             i.to_bytes(4, "little")
                             for i in inp.hd_keypaths[k][-2:]
                         ])
             inp.hd_keypaths = {}
     psbts['qrcode'] = qr_psbt.serialize()
     return psbts
 def create_psbts(self, base64_psbt, wallet):
     psbts = super().create_psbts(base64_psbt, wallet)
     qr_psbt = PSBT()
     # remove non-witness utxo if they are there to reduce QR code size
     updated_psbt = wallet.fill_psbt(base64_psbt,
                                     non_witness=False,
                                     xpubs=False)
     qr_psbt.deserialize(updated_psbt)
     # replace with compressed wallet information
     for inp in qr_psbt.inputs + qr_psbt.outputs:
         inp.witness_script = b""
         inp.redeem_script = b""
         if len(inp.hd_keypaths) > 0:
             k = list(inp.hd_keypaths.keys())[0]
             # proprietary field - wallet derivation path
             # only contains two last derivation indexes - change and index
             wallet_key = b"\xfc\xca\x01" + get_wallet_fingerprint(wallet)
             inp.unknown[wallet_key] = b"".join(
                 [i.to_bytes(4, "little") for i in inp.hd_keypaths[k][-2:]])
             inp.hd_keypaths = {}
     psbts['qrcode'] = qr_psbt.serialize()
     return psbts
Example #11
0
    def fill_psbt(self, b64psbt, non_witness: bool = True, xpubs: bool = True):
        psbt = PSBT()
        psbt.deserialize(b64psbt)
        if non_witness:
            for i, inp in enumerate(psbt.tx.vin):
                txid = inp.prevout.hash.to_bytes(32, "big").hex()
                try:
                    res = self.gettransaction(txid)
                    stream = BytesIO(bytes.fromhex(res["hex"]))
                    prevtx = CTransaction()
                    prevtx.deserialize(stream)
                    psbt.inputs[i].non_witness_utxo = prevtx
                except:
                    logger.error(
                        "Can't find previous transaction in the wallet. Signing might not be possible for certain devices..."
                    )
        else:
            # remove non_witness_utxo if we don't want them
            for inp in psbt.inputs:
                if inp.witness_utxo is not None:
                    inp.non_witness_utxo = None

        if xpubs:
            # for multisig add xpub fields
            if len(self.keys) > 1:
                for k in self.keys:
                    key = b"\x01" + decode_base58(k.xpub)
                    if k.fingerprint != "":
                        fingerprint = bytes.fromhex(k.fingerprint)
                    else:
                        fingerprint = get_xpub_fingerprint(k.xpub)
                    if k.derivation != "":
                        der = der_to_bytes(k.derivation)
                    else:
                        der = b""
                    value = fingerprint + der
                    psbt.unknown[key] = value
        return psbt.serialize()
Example #12
0
    def sign_tx(self, psbt: PSBT) -> Dict[str, str]:
        def find_our_key(
            keypaths: Dict[bytes, Sequence[int]]
        ) -> Tuple[Optional[bytes], Optional[Sequence[int]]]:
            """
            Keypaths is a map of pubkey to hd keypath, where the first element in the keypath is the master fingerprint. We attempt to find the key which belongs to the BitBox02 by matching the fingerprint, and then matching the pubkey.
            Returns the pubkey and the keypath, without the fingerprint.
            """
            for pubkey, keypath_with_fingerprint in keypaths.items():
                fp, keypath = keypath_with_fingerprint[0], keypath_with_fingerprint[1:]
                # Cheap check if the key is ours.
                if fp != master_fp:
                    continue

                # Expensive check if the key is ours.
                # TODO: check for fingerprint collision
                # keypath_account = keypath[:-2]

                return pubkey, keypath
            return None, None

        def get_simple_type(
            output: CTxOut, redeem_script: bytes
        ) -> bitbox02.btc.BTCScriptConfig.SimpleType:
            if is_p2pkh(output.scriptPubKey):
                raise BadArgumentError(
                    "The BitBox02 does not support legacy p2pkh scripts"
                )
            if is_p2wpkh(output.scriptPubKey):
                return bitbox02.btc.BTCScriptConfig.P2WPKH
            if output.is_p2sh() and is_p2wpkh(redeem_script):
                return bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
            raise BadArgumentError(
                "Input script type not recognized of input {}.".format(input_index)
            )

        master_fp = struct.unpack("<I", unhexlify(self.get_master_fingerprint_hex()))[0]

        inputs: List[bitbox02.BTCInputType] = []

        bip44_account = None

        # One pubkey per input. The pubkey identifies the key per input with which we sign. There
        # must be exactly one pubkey per input that belongs to the BitBox02.
        found_pubkeys: List[bytes] = []

        for input_index, (psbt_in, tx_in) in builtins.enumerate(
            zip(psbt.inputs, psbt.tx.vin)
        ):
            if psbt_in.sighash and psbt_in.sighash != 1:
                raise BadArgumentError(
                    "The BitBox02 only supports SIGHASH_ALL. Found sighash: {}".format(
                        psbt_in.sighash
                    )
                )

            utxo = None
            prevtx = None

            # psbt_in.witness_utxo was originally used for segwit utxo's, but since it was
            # discovered that the amounts are not correctly committed to in the segwit sighash, the
            # full prevtx (non_witness_utxo) is supplied for both segwit and non-segwit inputs.
            # See
            # - https://medium.com/shiftcrypto/bitbox-app-firmware-update-6-2020-c70f733a5330
            # - https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd.
            # - https://github.com/zkSNACKs/WalletWasabi/pull/3822
            # The BitBox02 for now requires the prevtx, at least until Taproot activates.

            if psbt_in.non_witness_utxo:
                if tx_in.prevout.hash != psbt_in.non_witness_utxo.sha256:
                    raise BadArgumentError(
                        "Input {} has a non_witness_utxo with the wrong hash".format(
                            input_index
                        )
                    )
                utxo = psbt_in.non_witness_utxo.vout[tx_in.prevout.n]
                prevtx = psbt_in.non_witness_utxo
            elif psbt_in.witness_utxo:
                utxo = psbt_in.witness_utxo
            if utxo is None:
                raise BadArgumentError("No utxo found for input {}".format(input_index))
            if prevtx is None:
                raise BadArgumentError(
                    "Previous transaction missing for input {}".format(input_index)
                )

            found_pubkey, keypath = find_our_key(psbt_in.hd_keypaths)
            if not found_pubkey:
                raise BadArgumentError("No key found for input {}".format(input_index))
            assert keypath is not None
            found_pubkeys.append(found_pubkey)

            # TOOD: validate keypath

            if bip44_account is None:
                bip44_account = keypath[2]
            elif bip44_account != keypath[2]:
                raise BadArgumentError(
                    "The bip44 account index must be the same for all inputs and changes"
                )

            simple_type = get_simple_type(utxo, psbt_in.redeem_script)

            script_config_index_map = {
                bitbox02.btc.BTCScriptConfig.P2WPKH: 0,
                bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH: 1,
            }

            inputs.append(
                {
                    "prev_out_hash": ser_uint256(tx_in.prevout.hash),
                    "prev_out_index": tx_in.prevout.n,
                    "prev_out_value": utxo.nValue,
                    "sequence": tx_in.nSequence,
                    "keypath": keypath,
                    "script_config_index": script_config_index_map[simple_type],
                    "prev_tx": {
                        "version": prevtx.nVersion,
                        "locktime": prevtx.nLockTime,
                        "inputs": [
                            {
                                "prev_out_hash": ser_uint256(prev_in.prevout.hash),
                                "prev_out_index": prev_in.prevout.n,
                                "signature_script": prev_in.scriptSig,
                                "sequence": prev_in.nSequence,
                            }
                            for prev_in in prevtx.vin
                        ],
                        "outputs": [
                            {
                                "value": prev_out.nValue,
                                "pubkey_script": prev_out.scriptPubKey,
                            }
                            for prev_out in prevtx.vout
                        ],
                    },
                }
            )

        outputs: List[bitbox02.BTCOutputType] = []
        for output_index, (psbt_out, tx_out) in builtins.enumerate(
            zip(psbt.outputs, psbt.tx.vout)
        ):
            _, keypath = find_our_key(psbt_out.hd_keypaths)
            is_change = keypath and keypath[-2] == 1
            if is_change:
                assert keypath is not None
                simple_type = get_simple_type(tx_out, psbt_out.redeem_script)
                outputs.append(
                    bitbox02.BTCOutputInternal(
                        keypath=keypath,
                        value=tx_out.nValue,
                        script_config_index=script_config_index_map[simple_type],
                    )
                )
            else:
                if tx_out.is_p2pkh():
                    output_type = bitbox02.btc.P2PKH
                    output_hash = tx_out.scriptPubKey[3:23]
                elif is_p2wpkh(tx_out.scriptPubKey):
                    output_type = bitbox02.btc.P2WPKH
                    output_hash = tx_out.scriptPubKey[2:]
                elif tx_out.is_p2sh():
                    output_type = bitbox02.btc.P2SH
                    output_hash = tx_out.scriptPubKey[2:22]
                elif is_p2wsh(tx_out.scriptPubKey):
                    output_type = bitbox02.btc.P2WSH
                    output_hash = tx_out.scriptPubKey[2:]
                else:
                    raise BadArgumentError(
                        "Output type not recognized of output {}".format(output_index)
                    )

                outputs.append(
                    bitbox02.BTCOutputExternal(
                        output_type=output_type,
                        output_hash=output_hash,
                        value=tx_out.nValue,
                    )
                )

        assert bip44_account is not None

        bip44_network = 1 + HARDENED if self.is_testnet else 0 + HARDENED
        sigs = self.init().btc_sign(
            bitbox02.btc.TBTC if self.is_testnet else bitbox02.btc.BTC,
            [
                bitbox02.btc.BTCScriptConfigWithKeypath(
                    script_config=bitbox02.btc.BTCScriptConfig(
                        simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH
                    ),
                    keypath=[84 + HARDENED, bip44_network, bip44_account],
                ),
                bitbox02.btc.BTCScriptConfigWithKeypath(
                    script_config=bitbox02.btc.BTCScriptConfig(
                        simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
                    ),
                    keypath=[49 + HARDENED, bip44_network, bip44_account],
                ),
            ],
            inputs=inputs,
            outputs=outputs,
            locktime=psbt.tx.nLockTime,
            version=psbt.tx.nVersion,
        )

        for (_, sig), pubkey, psbt_in in zip(sigs, found_pubkeys, psbt.inputs):
            r, s = sig[:32], sig[32:64]
            # ser_sig_der() adds SIGHASH_ALL
            psbt_in.partial_sigs[pubkey] = ser_sig_der(r, s)

        return {"psbt": psbt.serialize()}
Example #13
0
def send(args):
    # Load the wallet file
    wallet = load_wallet_file(args.wallet)

    # Load the watch only wallet
    rpc = LoadWalletAndGetRPC(args.wallet, wallet['rpcurl'])

    # Get a change address from the internal keypool
    change_addr = wallet['internal_keypool'][wallet['internal_next']]
    wallet['internal_next'] += 1

    # Write to the wallet
    write_wallet_to_file(args.wallet, wallet)

    # Create the transaction
    outputs = json.loads(args.recipients)
    locktime = rpc.getblockcount()
    psbtx = rpc.walletcreatefundedpsbt([], outputs, locktime, {'changeAddress' : change_addr, 'replaceable' : True, 'includeWatching' : True}, True)
    psbt = psbtx['psbt']

    # HACK: Hack to prevent change detection because hardware wallets are apparently really bad at change detection for multisig change
    tx = PSBT()
    tx.deserialize(psbt)
    for output in tx.outputs:
        output.set_null()
    psbt = tx.serialize()

    # Send psbt to devices to sign
    out = {}
    out['devices'] = []
    psbts = []
    for d in wallet['devices']:
        if 'core_wallet_name' in d:
            wrpc = LoadWalletAndGetRPC(d['core_wallet_name'], d['rpcurl'])
            result = wrpc.walletprocesspsbt(psbt)
            core_out = {'success' : True}
            out['core'] = core_out
            psbts.append(result['psbt'])
        else:
            d_out = {}

            hwi_args = []
            if args.testnet or args.regtest:
                hwi_args.append('--testnet')
            if 'password' in d:
                hwi_args.append('-p')
                hwi_args.append(d['password'])
            hwi_args.append('-f')
            hwi_args.append(d['fingerprint'])
            hwi_args.append('signtx')
            hwi_args.append(psbt)
            result = hwi_command(hwi_args)
            psbts.append(result['psbt'])
            d_out['fingerprint'] = d['fingerprint']
            d_out['success'] = True
            out['devices'].append(d_out)

    # Combine, finalize, and send psbts
    combined = rpc.combinepsbt(psbts)
    finalized = rpc.finalizepsbt(combined)
    if not finalized['complete']:
        out['success'] = False
        out['psbt'] = finalized['psbt']
        return out
    out['success'] = True
    if args.inspecttx:
        out['tx'] = finalized['hex']
    else:
        out['txid'] = rpc.sendrawtransaction(finalized['hex'])
    return out
Example #14
0
File: test_psbt.py Project: nvk/HWI
# Open the data file
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),
                       'data/test_psbt.json'),
          encoding='utf-8') as f:
    d = json.load(f)
    invalids = d['invalid']
    valids = d['valid']
    creators = d['creator']
    signers = d['signer']
    combiners = d['combiner']
    finalizers = d['finalizer']
    extractors = d['extractor']

print("Testing invalid PSBTs")
for invalid in invalids:
    try:
        psbt = PSBT()
        psbt.deserialize(invalid)
        assert False
    except:
        pass

print("Testing valid PSBTs")
for valid in valids:
    psbt = PSBT()
    psbt.deserialize(valid)
    serd = psbt.serialize()
    assert (valid == serd)

print("PSBT Serialization tests pass")