Exemplo n.º 1
0
    def verify(self):
        self._verified = False
        gen = None
        e = PSBTError("Invalid commitments")
        if self.asset and self.asset_commitment:
            # we can't verify asset
            if not self.asset_blinding_factor and not self.asset_proof:
                raise e
            gen = secp256k1.generator_parse(self.asset_commitment)
            # we have blinding factor
            if self.asset_blinding_factor:
                if gen != secp256k1.generator_generate_blinded(
                        self.asset, self.asset_blinding_factor):
                    raise e
            # otherwise use asset proof
            else:
                surj_proof = secp256k1.surjectionproof_parse(self.asset_proof)
                gen_asset = secp256k1.generator_generate(self.asset)
                if not secp256k1.surjectionproof_verify(
                        surj_proof, [gen_asset], gen):
                    raise e

        if self.value and self.value_commitment:
            if not gen or not (self.value_blinding_factor or self.value_proof):
                raise e
            # we have blinding factor
            if self.value_blinding_factor:
                value_commitment = secp256k1.pedersen_commit(
                    self.value_blinding_factor, self.value, gen)
                if self.value_commitment != secp256k1.pedersen_commitment_serialize(
                        value_commitment):
                    raise e
            # otherwise use value proof
            else:
                value_commitment = secp256k1.pedersen_commitment_parse(
                    self.value_commitment)
                min_value, max_value = secp256k1.rangeproof_verify(
                    self.value_proof,
                    value_commitment,
                    b"",
                    gen,
                )
                if (min_value != max_value) or (self.value != min_value):
                    raise e
        self._verified = True
        return self._verified
Exemplo n.º 2
0
 def verify(self):
     """Checks that all commitments, values and assets are consistent"""
     super().verify()
     for i, vout in enumerate(self.tx.vout):
         out = self.outputs[i]
         if out.is_blinded:
             gen = secp256k1.generator_generate_blinded(vout.asset[1:], out.asset_blinding_factor)
             if out.asset_commitment:
                 if secp256k1.generator_serialize(gen) != out.asset_commitment:
                     raise PSBTError("asset commitment is invalid")
             else:
                 out.asset_commitment = secp256k1.generator_serialize(gen)
             commit = secp256k1.pedersen_commit(out.value_blinding_factor, vout.value, gen)
             sec = secp256k1.pedersen_commitment_serialize(commit)
             if out.value_commitment:
                 if sec != out.value_commitment:
                     raise PSBTError("value commitment is invalid")
             else:
                 out.value_commitment = sec
Exemplo n.º 3
0
    def unblind(self, blinding_key):
        if self.range_proof is None:
            return

        pk = slip77.blinding_key(blinding_key, self.utxo.script_pubkey)

        value, asset, vbf, in_abf, extra, min_value, max_value = unblind(
            self.utxo.ecdh_pubkey, pk.secret, self.range_proof, self.utxo.value, self.utxo.asset, self.utxo.script_pubkey
        )
        # verify
        gen = secp256k1.generator_generate_blinded(asset, in_abf)
        assert gen == secp256k1.generator_parse(self.utxo.asset)
        cmt = secp256k1.pedersen_commit(vbf, value, gen)
        assert cmt == secp256k1.pedersen_commitment_parse(self.utxo.value)

        self.asset = asset
        self.value = value
        self.asset_blinding_factor = in_abf
        self.value_blinding_factor = vbf
Exemplo n.º 4
0
    def blind(self, seed:bytes):
        txseed = self.txseed(seed)
        # assign blinding factors to all outputs
        blinding_outs = []
        for i, out in enumerate(self.outputs):
            # skip ones where we don't need blinding
            if out.blinding_pubkey is None:
                continue
            out.asset_blinding_factor = hashes.tagged_hash("liquid/abf", txseed+i.to_bytes(4,'little'))
            out.value_blinding_factor = hashes.tagged_hash("liquid/vbf", txseed+i.to_bytes(4,'little'))
            blinding_outs.append(out)
        if len(blinding_outs) == 0:
            raise PSBTError("Nothing to blind")
        # calculate last vbf
        vals = [sc.value for sc in self.inputs + blinding_outs]
        abfs = [sc.asset_blinding_factor or b"\x00"*32 for sc in self.inputs + blinding_outs]
        vbfs = [sc.value_blinding_factor or b"\x00"*32 for sc in self.inputs + blinding_outs]
        last_vbf = secp256k1.pedersen_blind_generator_blind_sum(vals, abfs, vbfs, len(self.inputs))
        blinding_outs[-1].value_blinding_factor = last_vbf

        # calculate commitments (surj proof etc)

        in_tags = [inp.asset for inp in self.inputs]
        in_gens = [secp256k1.generator_parse(inp.utxo.asset) for inp in self.inputs]

        for i, out in enumerate(self.outputs):
            if out.blinding_pubkey is None:
                continue
            gen = secp256k1.generator_generate_blinded(out.asset, out.asset_blinding_factor)
            out.asset_commitment = secp256k1.generator_serialize(gen)
            value_commitment = secp256k1.pedersen_commit(out.value_blinding_factor, out.value, gen)
            out.value_commitment = secp256k1.pedersen_commitment_serialize(value_commitment)

            proof_seed = hashes.tagged_hash("liquid/surjection_proof", txseed+i.to_bytes(4,'little'))
            proof, in_idx = secp256k1.surjectionproof_initialize(in_tags, out.asset, seed=proof_seed)
            secp256k1.surjectionproof_generate(proof, in_idx, in_gens, gen, self.inputs[in_idx].asset_blinding_factor, out.asset_blinding_factor)
            out.surjection_proof = secp256k1.surjectionproof_serialize(proof)

            # generate range proof
            rangeproof_nonce = hashes.tagged_hash("liquid/range_proof", txseed+i.to_bytes(4,'little'))
            out.reblind(rangeproof_nonce)
Exemplo n.º 5
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
Exemplo n.º 6
0
    def blind(self, seed: bytes):
        txseed = self.txseed(seed)
        # assign blinding factors to all outputs
        blinding_outs = []
        for i, out in enumerate(self.outputs):
            # skip ones where we don't need blinding
            if out.blinding_pubkey is None or out.value is None:
                continue
            out.asset_blinding_factor = hashes.tagged_hash(
                "liquid/abf", txseed + i.to_bytes(4, 'little'))
            out.value_blinding_factor = hashes.tagged_hash(
                "liquid/vbf", txseed + i.to_bytes(4, 'little'))
            blinding_outs.append(out)
        if len(blinding_outs) == 0:
            raise PSBTError("Nothing to blind")
        # calculate last vbf
        vals = []
        abfs = []
        vbfs = []
        for sc in self.inputs + blinding_outs:
            value = sc.value if sc.value is not None else sc.utxo.value
            asset = sc.asset or sc.utxo.asset
            if not (isinstance(value, int) and len(asset) == 32):
                continue
            vals.append(value)
            abfs.append(sc.asset_blinding_factor or b"\x00" * 32)
            vbfs.append(sc.value_blinding_factor or b"\x00" * 32)
        last_vbf = secp256k1.pedersen_blind_generator_blind_sum(
            vals, abfs, vbfs,
            len(vals) - len(blinding_outs))
        blinding_outs[-1].value_blinding_factor = last_vbf

        # calculate commitments (surj proof etc)

        in_tags = []
        in_gens = []
        for inp in self.inputs:
            if inp.asset:
                in_tags.append(inp.asset)
                in_gens.append(secp256k1.generator_parse(inp.utxo.asset))
            # if we have unconfidential input
            elif len(inp.utxo.asset) == 32:
                in_tags.append(inp.utxo.asset)
                in_gens.append(secp256k1.generator_generate(inp.utxo.asset))

        for i, out in enumerate(self.outputs):
            if None in [
                    out.blinding_pubkey, out.value, out.asset_blinding_factor
            ]:
                continue
            gen = secp256k1.generator_generate_blinded(
                out.asset, out.asset_blinding_factor)
            out.asset_commitment = secp256k1.generator_serialize(gen)
            value_commitment = secp256k1.pedersen_commit(
                out.value_blinding_factor, out.value, gen)
            out.value_commitment = secp256k1.pedersen_commitment_serialize(
                value_commitment)

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

            # generate range proof
            rangeproof_nonce = hashes.tagged_hash(
                "liquid/range_proof", txseed + i.to_bytes(4, 'little'))
            out.reblind(rangeproof_nonce)

            # generate asset proof
            gen_asset = secp256k1.generator_generate(out.asset)
            proof, idx = secp256k1.surjectionproof_initialize([out.asset],
                                                              out.asset,
                                                              b"\x00" * 32, 1,
                                                              1)
            proof = secp256k1.surjectionproof_generate(
                proof, idx, [gen_asset], gen, b"\x00" * 32,
                out.asset_blinding_factor)
            out.asset_proof = secp256k1.surjectionproof_serialize(proof)

            # generate value proof
            value_proof_nonce = hashes.tagged_hash(
                "liquid/value_proof", txseed + i.to_bytes(4, 'little'))
            out.value_proof = secp256k1.rangeproof_sign(
                value_proof_nonce,
                out.value,
                value_commitment,
                out.value_blinding_factor,
                b"",
                b"",
                gen,
                out.value,  # min_value
                -1,  # exp
                0,  # min bits
            )