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
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
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
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)
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
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 )