Esempio n. 1
0
 def _copy_kv(self, fout, psbtv, key):
     # find offset of the key if it exists
     off = psbtv.seek_to_value(key, from_current=True)
     if off is None:
         return
     # we found it - copy over
     ser_string(fout, key)
     l = compact.read_from(psbtv.stream)
     fout.write(compact.to_bytes(l))
     read_write(psbtv.stream, fout, l)
     return off
Esempio n. 2
0
 def sign_message(self, derivation, msg: bytes, compressed: bool = True) -> bytes:
     """Sign message with private key"""
     msghash = sha256(
         sha256(
             b"\x18Bitcoin Signed Message:\n" + compact.to_bytes(len(msg)) + msg
         ).digest()
     ).digest()
     sig, flag = self.keystore.sign_recoverable(derivation, msghash)
     c = 4 if compressed else 0
     flag = bytes([27 + flag + c])
     ser = flag + secp256k1.ecdsa_signature_serialize_compact(sig._sig)
     return b2a_base64(ser).strip().decode()
Esempio n. 3
0
def aead_encrypt(key: bytes, adata: bytes = b"", plaintext: bytes = b"") -> bytes:
    """
    Encrypts and authenticates with associated data using key k.
    output format: <compact-len:associated data><iv><ct><hmac>
    """
    aes_key = tagged_hash("aes", key)
    hmac_key = tagged_hash("hmac", key)
    data = compact.to_bytes(len(adata)) + adata
    # if there is not ct - just add hmac
    if len(plaintext) > 0:
        data += encrypt(plaintext, aes_key)
    mac = hmac.new(hmac_key, msg=data, digestmod="sha256").digest()
    return data + mac
Esempio n. 4
0
    def preprocess_psbt(self, stream, fout):
        """
        Processes incoming PSBT, fills missing information and writes to fout.
        Returns:
        - wallets in inputs: list of tuples (wallet, amount)
        - metadata for tx display including warnings that require user confirmation
        """
        self.show_loader(title="Parsing transaction...")

        # compress = True flag will make sure large fields won't be loaded to RAM
        psbtv = self.PSBTViewClass.view(stream, compress=True)

        # Start with global fields of PSBT

        # On Liquid we check if txseed is provided (for deterministic blinding)
        # It will be None if it is not there.
        blinding_seed = psbtv.get_value(b"\xfc\x07specter\x00")
        if blinding_seed:
            hseed = hashes.tagged_hash_init("liquid/txseed", blinding_seed)
            vals = [] # values
            abfs = [] # asset blinding factors
            vbfs = [] # value blinding factors
            in_tags = []
            in_gens = []

        # Write global scope first
        psbtv.stream.seek(psbtv.offset)
        res = read_write(psbtv.stream, fout, psbtv.first_scope-psbtv.offset)

        # here we will store all wallets that we detect in inputs
        # wallet: {"amount": {asset: amount}, "gaps" [gaps]}
        wallets = {}
        meta = {
            "inputs": [{} for i in range(psbtv.num_inputs)],
            "outputs": [{} for i in range(psbtv.num_outputs)],
            "issuance": False, "reissuance": False,
        }

        fingerprint = self.keystore.fingerprint
        # We need to detect wallets owning inputs and outputs,
        # in case of liquid - unblind them.
        # Fill all necessary information:
        # For Bitcoin: bip32 derivations, witness script, redeem script
        # For Liquid: same + values, assets, commitments, proofs etc.
        # At the end we should have the most complete PSBT / PSET possible
        for i in range(psbtv.num_inputs):
            self.show_loader(title="Parsing input %d..." % i)
            # load input to memory, verify it (check prevtx hash)
            inp = psbtv.input(i)
            metainp = meta["inputs"][i]
            # verify, do not require non_witness_utxo if witness_utxo is set
            inp.verify(ignore_missing=True)

            # check sighash in the input
            if inp.sighash_type is not None and inp.sighash_type != self.DEFAULT_SIGHASH:
                metainp["sighash"] = self.get_sighash_info(inp.sighash_type)["name"]

            if inp.issue_value:
                if inp.issue_entropy:
                    meta["reissuance"] = True
                else:
                    meta["issuance"] = True

            # in Liquid we may need to rewind the rangeproof to get values
            rangeproof_offset = None

            off = psbtv.seek_to_scope(i)
            # find offset of the rangeproof if it exists
            rangeproof_offset = psbtv.seek_to_value(b'\xfc\x04pset\x0e', from_current=True)
            # add scope offset
            if rangeproof_offset is not None:
                rangeproof_offset += off

            # Find wallets owning the inputs and fill scope data:
            # first we check already detected wallet owns the input
            # as in most common case all inputs are owned by the same wallet.
            wallet = None
            for w in wallets:
                # pass rangeproof offset if it's in the scope
                if w and w.fill_scope(inp, fingerprint,
                                stream=psbtv.stream, rangeproof_offset=rangeproof_offset):
                    wallet = w
                    break
            # if it's a different wallet - go through all our wallets and check
            if wallet is None:
                # find wallet and append it to wallets
                for w in self.wallets:
                    # pass rangeproof offset if it's in the scope
                    if w.fill_scope(inp, fingerprint,
                                    stream=psbtv.stream, rangeproof_offset=rangeproof_offset):
                        wallet = w
                        break
            # get gaps
            gaps = None
            if wallet:
                gaps = [g for g in wallet.gaps] # copy
                res = wallet.get_derivation(inp.bip32_derivations)
                if res:
                    idx, branch_idx = res
                    gaps[branch_idx] = max(gaps[branch_idx], idx+wallet.GAP_LIMIT+1)
            # add wallet to tx wallets dict
            if wallet not in wallets:
                wallets[wallet] = {"amount": {}, "gaps": gaps}
            else:
                if wallets[wallet]["gaps"] is not None and gaps is not None:
                    wallets[wallet]["gaps"] = [max(g1,g2) for g1,g2 in zip(gaps, wallets[wallet]["gaps"])]

            # Get values (and assets) and store in metadata and wallets dict
            # we don't know yet if we unblinded the input or not, and if it was even blinded
            asset = inp.asset or inp.utxo.asset
            value = inp.value or inp.utxo.value
            # blinded assets are 33-bytes long, unblinded - 32
            if not (len(asset) == 32 and isinstance(value, int)):
                asset = None
                value = -1
                # if at least one input can't be unblinded - we can't generate proofs
                blinding_seed = None
            if blinding_seed:
                # update blinding seed
                hseed.update(bytes(reversed(inp.txid)))
                hseed.update(inp.vout.to_bytes(4,'little'))
                vals.append(value)
                abfs.append(inp.asset_blinding_factor or b"\x00"*32)
                vbfs.append(inp.value_blinding_factor or b"\x00"*32)
                in_tags.append(asset)
                if inp.utxo.asset is None:
                    raise WalletError("Missing input asset")
                if len(inp.utxo.asset) == 33:
                    in_gens.append(secp256k1.generator_parse(inp.utxo.asset))
                else:
                    in_gens.append(secp256k1.generator_generate(inp.utxo.asset))

            wallets[wallet]["amount"][asset] = wallets[wallet]["amount"].get(asset, 0) + value
            metainp.update({
                "label": wallet.name if wallet else "Unknown wallet",
                "value": value,
                "asset": self.asset_label(asset),
            })
            if wallet and wallet.is_watchonly:
                metainp["label"] += " (watch-only)"
            if asset not in self.assets:
                metainp.update({"raw_asset": asset})
            inp.write_to(fout, version=psbtv.version)

        # if blinding seed is set we can generate all proofs
        if blinding_seed:
            self.show_loader(title="Doing blinding magic...")
            blinding_out_indexes = []
            # first we go through all outputs and update the txseed
            for i in range(psbtv.num_outputs):
                out = psbtv.output(i)
                hseed.update(out.script_pubkey.serialize())
            txseed = hseed.digest()
            # now we can blind everything
            for i in range(psbtv.num_outputs):
                out = psbtv.output(i)
                if out.blinding_pubkey:
                    blinding_out_indexes.append(i)
                    abf = hashes.tagged_hash("liquid/abf", txseed+i.to_bytes(4,'little'))
                    vbf = hashes.tagged_hash("liquid/vbf", txseed+i.to_bytes(4,'little'))
                    abfs.append(abf)
                    vbfs.append(vbf)
                    vals.append(out.value)
            # get last vbf from scope
            out = psbtv.output(blinding_out_indexes[-1])
            if (None in vals or None in abfs or None in vbfs or None in in_tags):
                blinding_seed = None
            else:
                vbfs[-1] = secp256k1.pedersen_blind_generator_blind_sum(vals, abfs, vbfs, psbtv.num_inputs)
                # sanity check
                assert len(abfs) == psbtv.num_inputs + len(blinding_out_indexes)
                assert all([len(a)==32 for a in abfs])
                assert all([len(a)==32 for a in vbfs])

        memptr, memlen = get_preallocated_ram()
        # parse outputs and blind if necessary
        if blinding_seed:
            in_tags = b"".join(in_tags)
        for i in range(psbtv.num_outputs):
            gc.collect()
            self.show_loader(title="Parsing output %d..." % i)
            out = psbtv.output(i)
            metaout = meta["outputs"][i]
            # calculate commitments
            if blinding_seed and out.blinding_pubkey:
                # index of this output in the abfs, vbfs and vals
                list_idx = psbtv.num_inputs + blinding_out_indexes.index(i)
                self.show_loader(title="Generating asset commtiment %d..." % i)
                # asset commitment
                out.asset_blinding_factor = abfs[list_idx]
                gen = secp256k1.generator_generate_blinded(out.asset, out.asset_blinding_factor)
                out.asset_commitment = secp256k1.generator_serialize(gen)
                self.show_loader(title="Generating value commtiment %d..." % i)
                # value commitment
                out.value_blinding_factor = vbfs[list_idx]
                value_commitment = secp256k1.pedersen_commit(out.value_blinding_factor, out.value, gen)
                out.value_commitment = secp256k1.pedersen_commitment_serialize(value_commitment)

                # self.show_loader(title="Generating surjection proof %d..." % i)

                # # surjection proof
                # proof_seed = hashes.tagged_hash("liquid/surjection_proof", txseed+i.to_bytes(4,'little'))
                # plen, in_idx = secp256k1.surjectionproof_initialize_preallocated(memptr, memlen, in_tags, out.asset, proof_seed)
                # # proof, in_idx = secp256k1.surjectionproof_initialize(in_tags, out.asset, proof_seed)
                # secp256k1.surjectionproof_generate(memptr, in_idx, in_gens, gen, abfs[in_idx], out.asset_blinding_factor)
                # surjection_proof = secp256k1.surjectionproof_serialize(memptr)
                # # del proof

                # # write surjection proof
                # ser_string(fout, b'\xfc\x04pset\x05')
                # ser_string(fout, surjection_proof)
                # # del surjection_proof

                self.show_loader(title="Generating range proof %d..." % i)
                # generate range proof
                rangeproof_nonce = hashes.tagged_hash("liquid/range_proof", txseed+i.to_bytes(4,'little'))
                pub = secp256k1.ec_pubkey_parse(out.blinding_pubkey)
                out.ecdh_pubkey = ec.PrivateKey(rangeproof_nonce).sec()
                secp256k1.ec_pubkey_tweak_mul(pub, rangeproof_nonce)
                sec = secp256k1.ec_pubkey_serialize(pub)
                ecdh_nonce = hashes.double_sha256(sec)
                # proprietary field that stores extra message for recepient
                extra_message=out.unknown.get(b"\xfc\x07specter\x01", b"")
                msg = out.asset[-32:] + out.asset_blinding_factor + extra_message
                # write to temp file to get length first
                with open(self.tempdir+"/rangeproof_out", "wb") as frp:
                    rplen = secp256k1.rangeproof_sign_to(
                        frp, memptr, memlen,
                        ecdh_nonce, out.value, secp256k1.pedersen_commitment_parse(out.value_commitment),
                        out.value_blinding_factor, msg,
                        out.script_pubkey.data, secp256k1.generator_parse(out.asset_commitment)
                    )
                # write to fout rangeproof field
                with open(self.tempdir+"/rangeproof_out", "rb") as frp:
                    ser_string(fout, b'\xfc\x04pset\x04')
                    fout.write(compact.to_bytes(rplen))
                    read_write(frp, fout, rplen)

            rangeproof_offset = None
            # we only need to verify rangeproof if we didn't generate it ourselves
            if not blinding_seed:
                self.show_loader(title="Verifying output %d..." % i)
                # find rangeproof and surjection proof
                # rangeproof
                off = psbtv.seek_to_scope(psbtv.num_inputs+i)
                # find offset of the rangeproof if it exists
                rangeproof_offset = self._copy_kv(fout, psbtv, b'\xfc\x04pset\x04')
                if rangeproof_offset is None:
                    psbtv.seek_to_scope(psbtv.num_inputs+i)
                    # alternative key definition (psetv0)
                    rangeproof_offset = self._copy_kv(fout, psbtv, b'\xfc\x08elements\x04')
                if rangeproof_offset is not None:
                    rangeproof_offset += off

            surj_proof_offset = None
            # surjection proof
            off = psbtv.seek_to_scope(psbtv.num_inputs+i)
            # find offset of the rangeproof if it exists
            surj_proof_offset = self._copy_kv(fout, psbtv, b'\xfc\x04pset\x05')
            if surj_proof_offset is None:
                psbtv.seek_to_scope(psbtv.num_inputs+i)
                # alternative key definition (psetv0)
                surj_proof_offset = self._copy_kv(fout, psbtv, b'\xfc\x08elements\x05')
            if surj_proof_offset is not None:
                surj_proof_offset += off

            wallet = None
            for w in wallets:
                # pass rangeproof offset if it's in the scope
                if w and w.fill_scope(out, fingerprint,
                                stream=psbtv.stream,
                                rangeproof_offset=rangeproof_offset,
                ):
                    wallet = w
                    break
            if wallet is None:
                # find wallet and append it to wallets
                for w in self.wallets:
                    # pass rangeproof offset if it's in the scope
                    if w.fill_scope(out, fingerprint,
                                    stream=psbtv.stream,
                                    rangeproof_offset=rangeproof_offset,
                    ):
                        wallet = w
                        break
            # if we didn't blind it ourselves
            if not blinding_seed:
                try:
                    out.verify()
                except:
                    raise WalletError("Commitments in output %d are wrong" % i)

            # Get values (and assets) and store in metadata and wallets dict
            asset = out.asset or out.asset_commitment
            value = out.value or out.value_commitment
            # blinded assets are 33-bytes long, unblinded - 32
            if not (asset and value) or not (len(asset) == 32 and isinstance(value, int)):
                asset = None
                value = -1
            metaout.update({
                "change": (wallet is not None and len(wallets) == 1 and wallet in wallets),
                "value": value,
                "address": self.get_address(out),
                "asset": self.asset_label(asset),
            })
            if wallet:
                metaout["label"] = wallet.name
                res = wallet.get_derivation(out.bip32_derivations)
                if res:
                    idx, branch_idx = res
                    branch_txt = ""
                    if branch_idx == 1:
                        "change "
                    elif branch_idx > 1:
                        "branch %d " % branch_idx
                    metaout["label"] = "%s %s#%d" % (wallet.name, branch_txt, idx)
                    if wallet in wallets:
                        allowed_idx = wallets[wallet]["gaps"][branch_idx]
                    else:
                        allowed_idx = wallet.gaps[branch_idx]
                    if allowed_idx <= idx:
                        metaout["warning"] = "Derivation index is by %d larger than last known used index %d!" % (idx-allowed_idx+wallet.GAP_LIMIT, allowed_idx-wallet.GAP_LIMIT)
                if wallet.is_watchonly:
                    metaout["warning"] = "Watch-only wallet!"
            if asset and asset not in self.assets:
                metaout.update({"raw_asset": asset})
            out.write_to(fout, skip_separator=True, version=psbtv.version)
            # write rangeproofs and surjection proofs
            # separator
            fout.write(b"\x00")

        return wallets, meta
Esempio n. 5
0
    def preprocess_psbt(self, stream, fout):
        """
        Processes incoming PSBT, fills missing information and writes to fout.
        Returns:
        - wallets in inputs: dict {wallet: amount}
        - metadata for tx display including warnings that require user confirmation
        - default sighash to use for signing
        """
        self.show_loader(title="Parsing transaction...")

        # compress = True flag will make sure large fields won't be loaded to RAM
        psbtv = self.PSBTViewClass.view(stream, compress=True)

        # Write global scope first
        psbtv.stream.seek(psbtv.offset)
        res = read_write(psbtv.stream, fout, psbtv.first_scope-psbtv.offset)

        # string representation of the Bitcoin for wallet processing
        fee = 0

        # here we will store all wallets that we detect in inputs
        # {wallet: {"amount": amount, "gaps": [gaps]}}
        wallets = {}
        meta = {
            "inputs": [{} for i in range(psbtv.num_inputs)],
            "outputs": [{} for i in range(psbtv.num_outputs)],
            "default_asset": "BTC" if self.network == "main" else "tBTC",
        }

        fingerprint = self.keystore.fingerprint
        # We need to detect wallets owning inputs and outputs,
        # in case of liquid - unblind them.
        # Fill all necessary information:
        # For Bitcoin: bip32 derivations, witness script, redeem script
        # For Liquid: same + values, assets, commitments, proofs etc.
        # At the end we should have the most complete PSBT / PSET possible
        for i in range(psbtv.num_inputs):
            self.show_loader(title="Parsing input %d..." % i)
            # load input to memory, verify it (check prevtx hash)
            inp = psbtv.input(i)
            metainp = meta["inputs"][i]
            # verify, do not require non_witness_utxo if witness_utxo is set
            inp.verify(ignore_missing=True)

            # check sighash in the input
            if inp.sighash_type is not None and inp.sighash_type != self.DEFAULT_SIGHASH:
                metainp["sighash"] = self.get_sighash_info(inp.sighash_type)["name"]

            self.fill_zero_fingerprint(inp)

            # Find wallets owning the inputs and fill scope data:
            # first we check already detected wallet owns the input
            # as in most common case all inputs are owned by the same wallet.
            wallet = None
            gaps = None
            for w in wallets:
                # pass rangeproof offset if it's in the scope
                if w and w.fill_scope(inp, fingerprint):
                    wallet = w
                    break
            if wallet is None:
                # find wallet and append it to wallets
                for w in self.wallets:
                    # pass rangeproof offset if it's in the scope
                    if w.fill_scope(inp, fingerprint):
                        wallet = w
                        break
            if wallet:
                gaps = [g for g in wallet.gaps] # copy
                res = wallet.get_derivation(inp.bip32_derivations)
                if res:
                    idx, branch_idx = res
                    gaps[branch_idx] = max(gaps[branch_idx], idx+wallet.GAP_LIMIT+1)
            # add wallet to tx wallets dict
            if wallet not in wallets:
                wallets[wallet] = {"amount": 0, "gaps": gaps}
            else:
                if wallets[wallet]["gaps"] is not None and gaps is not None:
                    wallets[wallet]["gaps"] = [max(g1,g2) for g1,g2 in zip(gaps, wallets[wallet]["gaps"])]

            value = inp.utxo.value
            fee += value

            wallets[wallet]["amount"] = wallets.get(wallet, {}).get("amount") + value
            metainp.update({
                "label": wallet.name if wallet else "Unknown wallet",
                "value": value,
            })
            if wallet and wallet.is_watchonly:
                metainp["label"] += " (watch-only)"
            # write non_witness_utxo separately if it exists (as we use compressed psbtview)
            non_witness_utxo_off = None
            off = psbtv.seek_to_scope(i)
            non_witness_utxo_off = psbtv.seek_to_value(b'\x00', from_current=True)
            if non_witness_utxo_off:
                non_witness_utxo_off += off
                l = compact.read_from(psbtv.stream)
                fout.write(b"\x01\x00")
                fout.write(compact.to_bytes(l))
                read_write(psbtv.stream, fout, l)
            inp.write_to(fout, version=psbtv.version)

        # parse all outputs
        for i in range(psbtv.num_outputs):
            self.show_loader(title="Parsing output %d..." % i)
            out = psbtv.output(i)
            metaout = meta["outputs"][i]

            self.fill_zero_fingerprint(out)

            wallet = None
            for w in wallets:
                # pass rangeproof offset if it's in the scope
                if w and w.fill_scope(out, fingerprint):
                    wallet = w
                    break
            if wallet is None:
                # find wallet and append it to wallets
                for w in self.wallets:
                    if w.fill_scope(out, fingerprint):
                        wallet = w
                        break
            # Get values and store in metadata and wallets dict
            value = out.value
            fee -= value
            metaout.update({
                "change": (wallet is not None and len(wallets) == 1 and wallet in wallets),
                "value": value,
                "address": self.get_address(out),
            })
            if wallet:
                metaout["label"] = wallet.name
                res = wallet.get_derivation(out.bip32_derivations)
                if res:
                    idx, branch_idx = res
                    branch_txt = ""
                    if branch_idx == 1:
                        "change "
                    elif branch_idx > 1:
                        "branch %d " % branch_idx
                    metaout["label"] = "%s %s#%d" % (wallet.name, branch_txt, idx)
                    if wallet in wallets:
                        allowed_idx = wallets[wallet]["gaps"][branch_idx]
                    else:
                        allowed_idx = wallet.gaps[branch_idx]
                    if allowed_idx <= idx:
                        metaout["warning"] = "Derivation index is by %d larger than last known used index %d!" % (idx-allowed_idx+wallet.GAP_LIMIT, allowed_idx-wallet.GAP_LIMIT)
                if wallet.is_watchonly:
                    metaout["warning"] = "Watch-only wallet!"

            out.write_to(fout, version=psbtv.version)
        meta["fee"] = fee
        return wallets, meta