def test_grind(self): pk = PrivateKey(b"1" * 32) msgs = [bytes([i] * 32) for i in range(255)] for msg in msgs: sig = pk.sign(msg) der = sig.serialize() # low R is grinded self.assertTrue(len(der) <= 70)
def test_pubkeys(self): valid_keys = [ ( b"1" * 32, True, "036930f46dd0b16d866d59d1054aa63298b357499cd1862ef16f3f55f1cafceb82", ), ( b"\x00" * 31 + b"\x01", False, "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", ), ] pub2 = PublicKey.from_string( "026930f46dd0b16d866d59d1054aa63298b357499cd1862ef16f3f55f1cafceb82" ) for secret, compressed, sec in valid_keys: priv = PrivateKey(secret, compressed=compressed) pub = priv.get_public_key() # check str works str(priv) str(pub) self.assertEqual(str(pub), sec) self.assertEqual(pub, PublicKey.from_string(sec)) pub.compressed = not pub.compressed # compressed and uncompressed are considered different now self.assertFalse(pub == PublicKey.from_string(sec)) s = BytesIO() self.assertEqual(pub.write_to(s), 33 + 32 * int(not pub.compressed)) self.assertEqual(priv.write_to(s), 32) # round trip self.assertEqual(PrivateKey.parse(priv.serialize()), priv) self.assertEqual(PublicKey.parse(pub.serialize()), pub) # sign random message msg = b"5" * 32 sig = priv.sign(msg) self.assertTrue(pub.verify(sig, msg)) # round trip self.assertEqual(Signature.parse(sig.serialize()), sig) # checks of the operators self.assertEqual(priv < pub, priv.sec() < pub.sec()) self.assertEqual(priv > pub, priv.sec() > pub.sec()) self.assertEqual(pub2 < pub, pub2 < priv) self.assertEqual(pub2 > pub, pub2 > priv) priv == priv pub == pub self.assertEqual(str(priv), priv.wif()) self.assertEqual(str(priv), priv.to_base58()) hash(priv) hash(pub)
def sign_with_descriptor(self, d1, d2, root, selfblind=False): rpc = daemon.rpc wname = random_wallet_name() # to derive addresses desc1 = Descriptor.from_string(d1) desc2 = Descriptor.from_string(d2) # recv addr 2 addr1 = desc1.derive(2).address(net) # change addr 3 addr2 = desc2.derive(3).address(net) # to add checksums d1 = add_checksum(str(d1)) d2 = add_checksum(str(d2)) rpc.createwallet(wname, True, True, "", False, True, False) w = daemon.wallet(wname) res = w.importdescriptors([{ "desc": d1, "active": True, "internal": False, "timestamp": "now", }, { "desc": d2, "active": True, "internal": True, "timestamp": "now", }]) self.assertTrue(all([k["success"] for k in res])) bpk = b"1" * 32 w.importmasterblindingkey(bpk.hex()) addr1 = w.getnewaddress() wdefault = daemon.wallet() wdefault.sendtoaddress(addr1, 0.1) daemon.mine() waddr = wdefault.getnewaddress() psbt = w.walletcreatefundedpsbt([], [{ waddr: 0.002 }], 0, { "includeWatching": True, "changeAddress": addr1, "fee_rate": 1 }, True) unsigned = psbt["psbt"] # fix blinding change address tx = PSBT.from_string(unsigned) _, bpub = addr_decode(addr1) if not tx.outputs[psbt["changepos"]].blinding_pubkey: tx.outputs[psbt["changepos"]].blinding_pubkey = bpub.sec() unsigned = str(tx) # blind with custom message if selfblind: unblinded_psbt = PSBT.from_string(unsigned) # generate all blinding stuff unblinded_psbt.unblind( PrivateKey(bpk)) # get values and blinding factors for inputs unblinded_psbt.blind( os.urandom(32)) # generate all blinding factors etc for i, out in enumerate(unblinded_psbt.outputs): if unblinded_psbt.outputs[i].blinding_pubkey: out.reblind(b"1" * 32, unblinded_psbt.outputs[i].blinding_pubkey, b"test message") # remove stuff that Core doesn't like for inp in unblinded_psbt.inputs: inp.value = None inp.asset = None inp.value_blinding_factor = None inp.asset_blinding_factor = None for out in unblinded_psbt.outputs: if out.is_blinded: out.asset = None out.asset_blinding_factor = None out.value = None out.value_blinding_factor = None psbt = unblinded_psbt # use rpc to blind transaction else: try: # master branch blinded = w.blindpsbt(unsigned) except: blinded = w.walletprocesspsbt(unsigned)['psbt'] psbt = PSBT.from_string(blinded) psbt.sign_with(root) final = rpc.finalizepsbt(str(psbt)) if final["complete"]: raw = final["hex"] else: print("WARNING: finalize failed, trying with embit") tx = finalize_psbt(psbt) raw = str(tx) # test accept res = rpc.testmempoolaccept([raw]) self.assertTrue(res[0]["allowed"]) if selfblind: # check we can reblind all outputs import json raw = w.unblindrawtransaction(raw)["hex"] decoded = w.decoderawtransaction(raw) self.assertEqual( len(decoded["vout"]) - sum([int("value" in out) for out in decoded["vout"]]), 1)
def _unblind(self): if not self.descriptor.is_blinded: return b = self.tx mbpk = self.descriptor.blinding_key.key net = self.network values = [0 for out in b.vout] assets = [b"\xFF" * 32 for out in b.vout] datas = [] # search for datas encoded in rangeproofs for i, out in enumerate(b.vout): # unblinded if isinstance(out.value, int): values[i] = out.value assets[i] = out.asset continue pk = slip77.blinding_key(mbpk, out.script_pubkey) try: res = out.unblind(pk.secret, message_length=1000) value, asset, vbf, abf, extra, *_ = res if len(extra.rstrip(b"\x00")) > 0: datas.append(extra) values[i] = value assets[i] = asset except Exception as e: logger.warn(e) # TODO: remove, it's ok pass # to calculate blinding seed tx = PSET(b) seed = tagged_hash("liquid/blinding_seed", mbpk.secret) txseed = tx.txseed(seed) pubkeys = {} for extra in datas: s = BytesIO(extra) while True: k = read_string(s) if len(k) == 0: break v = read_string(s) if k[0] == 1 and len(k) == 5: idx = int.from_bytes(k[1:], "little") pubkeys[idx] = v elif k == b"\x01\x00": txseed = v for i, out in enumerate(b.vout): if out.witness.range_proof.is_empty: continue if i in pubkeys and len(pubkeys[i]) in [33, 65]: nonce = tagged_hash("liquid/range_proof", txseed + i.to_bytes(4, "little")) if out.ecdh_pubkey == PrivateKey(nonce).sec(): try: res = unblind( pubkeys[i], nonce, out.witness.range_proof.data, out.value, out.asset, out.script_pubkey, ) value, asset, vbf, abf, extra, min_value, max_value = res assets[i] = asset values[i] = value except Exception as e: logger.warn(f"Failed at unblinding output {i}: {e}") else: logger.warn(f"Failed at unblinding: {e}") for i, out in enumerate(b.vout): out.asset = assets[i] out.value = values[i] out.witness = TxOutWitness()
def decoderawtransaction(self, tx): unblinded = self.unblindrawtransaction(tx)["hex"] obj = super().__getattr__("decoderawtransaction")(unblinded) try: # unblind the rest of outputs b = LTransaction.from_string(tx) mbpk = PrivateKey(bytes.fromhex(self.dumpmasterblindingkey())) net = get_network(self.getblockchaininfo().get("chain")) outputs = obj["vout"] datas = [] fee = 0 # search for datas encoded in rangeproofs for i, out in enumerate(b.vout): o = outputs[i] if isinstance(out.value, int): if "value" in o: assert o["value"] == round(out.value * 1e-8, 8) else: o["value"] = round(out.value * 1e-8, 8) if "asset" in o: assert o["asset"] == bytes(reversed( out.asset[-32:])).hex() else: o["asset"] = bytes(reversed(out.asset[-32:])).hex() try: o["scriptPubKey"]["addresses"] = [ liquid_address(out.script_pubkey, network=net) ] except: pass if out.script_pubkey.data == b"": # fee negative? fee -= out.value pk = slip77.blinding_key(mbpk, out.script_pubkey) try: res = out.unblind(pk.secret, message_length=1000) value, asset, vbf, abf, extra, min_value, max_value = res if "value" in o: assert o["value"] == round(value * 1e-8, 8) else: o["value"] = round(value * 1e-8, 8) if "asset" in o: assert o["asset"] == bytes(reversed(asset[-32:])).hex() else: o["asset"] = bytes(reversed(asset[-32:])).hex() try: o["scriptPubKey"]["addresses"] = [ liquid_address(out.script_pubkey, pk, network=net) ] except: pass if len(extra.rstrip(b"\x00")) > 0: datas.append(extra) except Exception as e: pass # should be changed with seed from tx tx = PSET(b) seed = tagged_hash("liquid/blinding_seed", mbpk.secret) txseed = tx.txseed(seed) pubkeys = {} for extra in datas: s = BytesIO(extra) while True: k = read_string(s) if len(k) == 0: break v = read_string(s) if k[0] == 1 and len(k) == 5: idx = int.from_bytes(k[1:], "little") pubkeys[idx] = v elif k == b"\x01\x00": txseed = v for i, out in enumerate(outputs): o = out if i in pubkeys and len(pubkeys[i]) in [33, 65]: nonce = tagged_hash("liquid/range_proof", txseed + i.to_bytes(4, "little")) if b.vout[i].ecdh_pubkey == PrivateKey(nonce).sec(): try: res = unblind( pubkeys[i], nonce, b.vout[i].witness.range_proof.data, b.vout[i].value, b.vout[i].asset, b.vout[i].script_pubkey, ) value, asset, vbf, abf, extra, min_value, max_value = res if "value" in o: assert o["value"] == round(value * 1e-8, 8) else: o["value"] = round(value * 1e-8, 8) if "asset" in o: assert o["asset"] == bytes( reversed(asset[-32:])).hex() else: o["asset"] = bytes(reversed(asset[-32:])).hex() try: o["scriptPubKey"]["addresses"] = [ liquid_address( b.vout[i].script_pubkey, PublicKey.parse(pubkeys[i]), network=net, ) ] except: pass except Exception as e: logger.warn( f"Failed at unblinding output {i}: {e}") else: logger.warn(f"Failed at unblinding: {e}") if fee != 0: obj["fee"] = round(-fee * 1e-8, 8) except Exception as e: logger.warn(f"Failed at unblinding transaction: {e}") return obj
def walletcreatefundedpsbt(self, inputs, outputs, *args, blind=True, **kwargs): """ Creates and blinds an Elements PSBT transaction. Arguments: 1. inputs: [{txid, vout[, sequence, pegin stuff]}] 2. outputs: [{address: amount, "asset": asset}, ...] # TODO: add assets support 3. locktime = 0 4. options {includeWatching, changeAddress, subtractFeeFromOutputs, replaceable, add_inputs, feeRate, fee_rate} 5. bip32 derivations 6. solving data 7. blind = True - Specter-LiquidRPC specific thing - blind transaction after creation """ res = super().__getattr__("walletcreatefundedpsbt")(inputs, outputs, *args, **kwargs) psbt = res.get("psbt", None) # check if we should blind the transaction if psbt and blind: # check that change is also blinded - fixes a bug in pset branch tx = PSET.from_string(psbt) der = None changepos = res.get("changepos", None) if changepos is not None and len(args) >= 2: addr = args[1].get("changeAddress", None) if addr: _, bpub = addr_decode(addr) der = tx.outputs[changepos].bip32_derivations if bpub and (tx.outputs[changepos].blinding_pubkey is None): tx.outputs[changepos].blinding_pubkey = bpub.sec() res["psbt"] = str(tx) psbt = str(tx) # generate all blinding stuff ourselves in deterministic way bpk = bytes.fromhex(self.dumpmasterblindingkey()) tx.unblind( PrivateKey(bpk)) # get values and blinding factors for inputs seed = tagged_hash("liquid/blinding_seed", bpk) tx.blind(seed) # generate all blinding factors etc # proprietary fields for Specter - 00 is global blinding seed tx.unknown[b"\xfc\x07specter\x00"] = seed # reblind and encode nonces in change output if changepos is not None: txseed = tx.txseed(seed) # blinding seed to calculate per-output nonces message = b"\x01\x00\x20" + txseed for i, out in enumerate(tx.outputs): # skip unblinded and change address itself if out.blinding_pubkey is None or i == changepos: continue # key 01<i> is blinding pubkey for output i message += b"\x05\x01" + i.to_bytes(4, "little") # message is blinding pubkey message += bytes([len(out.blinding_pubkey) ]) + out.blinding_pubkey # extra message for rangeproof - proprietary field tx.outputs[changepos].unknown[b"\xfc\x07specter\x01"] = message # re-generate rangeproof with extra message nonce = tagged_hash("liquid/range_proof", txseed + changepos.to_bytes(4, "little")) tx.outputs[changepos].reblind(nonce, extra_message=message) res["psbt"] = str(tx) return res
def master_blinding_key(self): if self._master_blinding_key is None: self._master_blinding_key = PrivateKey( bytes.fromhex(self.dumpmasterblindingkey()) ) return self._master_blinding_key