def combinepsbt(self, psbts, *args, **kwargs): if len(psbts) == 0: raise RpcError("Provide at least one psbt") tx = PSET.from_string(psbts[0]) for b64 in psbts[1:]: t2 = PSET.from_string(b64) tx.version = tx.version or t2.version tx.tx_version = tx.tx_version or t2.tx_version tx.locktime = tx.locktime or t2.locktime tx.xpubs.update(t2.xpubs) tx.unknown.update(t2.unknown) for inp1, inp2 in zip(tx.inputs, t2.inputs): inp1.value = inp1.value or inp2.value inp1.value_blinding_factor = ( inp1.value_blinding_factor or inp2.value_blinding_factor ) inp1.asset = inp1.asset or inp2.asset inp1.asset_blinding_factor = ( inp1.asset_blinding_factor or inp2.asset_blinding_factor ) inp1.txid = inp1.txid or inp2.txid inp1.vout = inp1.vout or inp2.vout inp1.sequence = inp1.sequence or inp2.sequence inp1.non_witness_utxo = inp1.non_witness_utxo or inp2.non_witness_utxo inp1.sighash_type = inp1.sighash_type or inp2.sighash_type inp1.redeem_script = inp1.redeem_script or inp2.redeem_script inp1.witness_script = inp1.witness_script or inp2.witness_script inp1.final_scriptsig = inp1.final_scriptsig or inp2.final_scriptsig inp1.final_scriptwitness = ( inp1.final_scriptwitness or inp2.final_scriptwitness ) inp1.partial_sigs.update(inp2.partial_sigs) inp1.bip32_derivations.update(inp2.bip32_derivations) inp1.unknown.update(inp2.unknown) inp1.range_proof = inp1.range_proof or inp2.range_proof for out1, out2 in zip(tx.outputs, t2.outputs): out1.value_commitment = out1.value_commitment or out2.value_commitment out1.value_blinding_factor = ( out1.value_blinding_factor or out2.value_blinding_factor ) out1.asset_commitment = out1.asset_commitment or out2.asset_commitment out1.asset_blinding_factor = ( out1.asset_blinding_factor or out2.asset_blinding_factor ) out1.range_proof = out1.range_proof or out2.range_proof out1.surjection_proof = out1.surjection_proof or out2.surjection_proof out1.ecdh_pubkey = out1.ecdh_pubkey or out2.ecdh_pubkey out1.blinding_pubkey = out1.blinding_pubkey or out2.blinding_pubkey out1.asset = out1.asset or out2.asset out1.value = out1.value or out2.value out1.script_pubkey = out1.script_pubkey or out2.script_pubkey out1.unknown = out1.unknown or out2.unknown out1.redeem_script = out1.redeem_script or out2.redeem_script out1.witness_script = out1.witness_script or out2.witness_script out1.bip32_derivations.update(out2.bip32_derivations) out1.unknown.update(out2.unknown) return str(tx)
def decodepsbt(self, b64psbt, *args, **kwargs): tx = PSET.from_string(b64psbt) inputs = [(inp.value, inp.asset) for inp in tx.inputs] for inp in tx.inputs: inp.value = None inp.asset = None inp.value_blinding_factor = None inp.asset_blinding_factor = None for out in tx.outputs: if out.asset and out.value: # out.asset = None out.asset_blinding_factor = None # out.value = None out.value_blinding_factor = None out.asset_commitment = None out.value_commitment = None out.range_proof = None out.surjection_proof = None out.ecdh_pubkey = None b64psbt = str(tx) decoded = super().__getattr__("decodepsbt")(b64psbt, *args, **kwargs) # pset branch - no fee and global tx fields... if "tx" not in decoded or "fee" not in decoded: pset = PSET.from_string(b64psbt) if "tx" not in decoded: decoded["tx"] = self.decoderawtransaction(str(pset.tx)) if "fee" not in decoded: decoded["fee"] = pset.fee() * 1e-8 for out in decoded["outputs"]: if "value" not in out: out["value"] = -1 for out in decoded["tx"]["vout"]: if "value" not in out: out["value"] = -1 for i, (v, a) in enumerate(inputs): inp = decoded["tx"]["vin"][i] # old psbt inp2 = decoded["inputs"][i] # new psbt if "utxo_rangeproof" in inp2: inp2.pop("utxo_rangeproof") a = bytes(reversed(a[-32:])).hex() v = round(v * 1e-8, 8) if "value" not in inp: inp["value"] = v if "asset" not in inp: inp["asset"] = a if "value" not in inp2: inp2["value"] = v if "asset" not in inp2: inp2["asset"] = a return decoded
def test_pset(self): psets = [ "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", ] for b64 in psets: tx = PSET.from_string(b64) self.assertTrue(str(tx), b64)
def create_psbts(self, base64_psbt, wallet): try: # remove rangeproofs and add sighash alls psbt = PSET.from_string(base64_psbt) for out in psbt.outputs: out.range_proof = None out.surjection_proof = None for inp in psbt.inputs: if not inp.sighash_type: inp.sighash_type = LSIGHASH.ALL base64_psbt = psbt.to_string() except: pass psbts = super().create_psbts(base64_psbt, wallet) # 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) try: qr_psbt = PSBT.from_string(updated_psbt) except: qr_psbt = PSET.from_string(updated_psbt) # find my key fgp = None derivation = None for k in wallet.keys: if k in self.keys and k.fingerprint and k.derivation: fgp = bytes.fromhex(k.fingerprint) derivation = bip32.parse_path(k.derivation) break # remove unnecessary derivations from inputs and outputs for inp in qr_psbt.inputs + qr_psbt.outputs: # keep only my derivation for k in list(inp.bip32_derivations.keys()): if fgp and inp.bip32_derivations[k].fingerprint != fgp: inp.bip32_derivations.pop(k, None) # remove scripts from outputs (DIY should know about the wallet) for out in qr_psbt.outputs: out.witness_script = None out.redeem_script = None # remove partial sigs from inputs for inp in qr_psbt.inputs: inp.partial_sigs = {} psbts["qrcode"] = qr_psbt.to_string() # we can add xpubs to SD card, but non_witness can be too large for MCU psbts["sdcard"] = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=True) return psbts
def clean_psbt(b64psbt): try: psbt = PSBT.from_string(b64psbt) except: psbt = PSET.from_string(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.to_string()
def finalizepsbt(self, psbt, *args, **kwargs): psbt = to_canonical_pset(psbt) res = super().__getattr__("finalizepsbt")(psbt, *args, **kwargs) if res["complete"] == False: try: # try using our finalizer tx = finalizer.finalize_psbt(PSET.from_string(psbt)) if tx and self.testmempoolaccept([str(tx)]): return {"complete": True, "hex": str(tx)} except Exception as e: logger.exception(e) return res
def sign_pset(self, b64pset: str) -> str: """Signs specter-desktop specific Liquid PSET transaction""" mfp = self.get_master_fingerprint() pset = PSET.from_string(b64pset) commitments = self._blind(pset) ins = [ { "is_witness": True, # "input_tx": inp.non_witness_utxo.serialize(), "script": inp.witness_script.data if inp.witness_script else script.p2pkh_from_p2wpkh(inp.script_pubkey).data, "value_commitment": write_commitment(inp.utxo.value), "path": [ der for der in inp.bip32_derivations.values() if der.fingerprint == mfp ][0].derivation, } for inp in pset.inputs ] change = [ { "path": [ der for pub, der in out.bip32_derivations.items() if der.fingerprint == mfp ][0].derivation, "variant": self._get_script_type(out), } if out.bip32_derivations and self._get_script_type(out) is not None else None for out in pset.outputs ] rawtx = pset.blinded_tx.serialize() signatures = self.jade.sign_liquid_tx( self._network(), rawtx, ins, commitments, change ) for i, inp in py_enumerate(pset.inputs): inp.partial_sigs[ [ pub for pub, der in inp.bip32_derivations.items() if der.fingerprint == mfp ][0] ] = signatures[i] # we must finalize here because it has different commitments and only supports singlesig return str(finalize_psbt(pset))
def _cleanpsbt(self, psbt): """Removes stuff that Core doesn't like""" tx = PSET.from_string(psbt) for inp in tx.inputs: inp.value = None inp.asset = None inp.value_blinding_factor = None inp.asset_blinding_factor = None for out in tx.outputs: if out.is_blinded: out.asset = None out.asset_blinding_factor = None out.value = None out.value_blinding_factor = None return str(tx)
def decodepsbt(self, b64psbt, *args, **kwargs): decoded = super().__getattr__("decodepsbt")(b64psbt, *args, **kwargs) # pset branch - no fee and global tx fields... if "tx" not in decoded or "fee" not in decoded: pset = PSET.from_string(b64psbt) if "tx" not in decoded: decoded["tx"] = self.decoderawtransaction(str(pset.tx)) if "fee" not in decoded: decoded["fee"] = pset.fee() * 1e-8 for out in decoded["outputs"]: if "value" not in out: out["value"] = -1 for out in decoded["tx"]["vout"]: if "value" not in out: out["value"] = -1 return decoded
def fill_psbt(self, b64psbt, non_witness: bool = True, xpubs: bool = True): psbt = PSET.from_string(b64psbt) if non_witness: for inp in psbt.inputs: # we don't need to fill what is already filled if inp.non_witness_utxo is not None: continue txid = inp.txid.hex() try: res = self.gettransaction(txid) inp.non_witness_utxo = Transaction.from_string(res["hex"]) except Exception as e: logger.error( f"Can't find previous transaction in the wallet. Signing might not be possible for certain devices... Txid: {txid}, Exception: {e}" ) 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 = bip32.HDKey.from_string(k.xpub) if k.fingerprint != "": fingerprint = bytes.fromhex(k.fingerprint) else: fingerprint = get_xpub_fingerprint(k.xpub) if k.derivation != "": der = bip32.parse_path(k.derivation) else: der = [] psbt.xpub[key] = DerivationPath(fingerprint, der) else: psbt.xpub = {} return psbt.to_string()
def to_canonical_pset(pset: str) -> str: """ Removes unblinded information from the transaction so Elements Core can decode it """ # if we got psbt, not pset - just return if not pset.startswith("cHNl"): return pset tx = PSET.from_string(pset) for inp in tx.inputs: inp.value = None inp.asset = None inp.value_blinding_factor = None inp.asset_blinding_factor = None for out in tx.outputs: if out.is_blinded: out.asset = None out.asset_blinding_factor = None out.value = None out.value_blinding_factor = None return str(tx)
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 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 create_psbts(self, base64_psbt, wallet): # liquid transaction if base64_psbt.startswith("cHNl"): # remove rangeproofs and add sighash alls psbt = PSET.from_string(base64_psbt) # make sure we have tx blinding seed in the transaction if psbt.unknown.get(b"\xfc\x07specter\x00"): for out in psbt.outputs: out.range_proof = None # out.surjection_proof = None # we know assets - we can blind it if out.asset: out.asset_commitment = None out.asset_blinding_factor = None # we know value - we can blind it if out.value: out.value_commitment = None out.value_blinding_factor = None for inp in psbt.inputs: if inp.value and inp.asset: inp.range_proof = None else: psbt = PSBT.from_string(base64_psbt) fill_external_wallet_derivations(psbt, wallet) base64_psbt = psbt.to_string() psbts = super().create_psbts(base64_psbt, wallet) # 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, taproot_derivations=True) try: qr_psbt = PSBT.from_string(updated_psbt) except: qr_psbt = PSET.from_string(updated_psbt) # find my key fgp = None derivation = None for k in wallet.keys: if k in self.keys and k.fingerprint and k.derivation: fgp = bytes.fromhex(k.fingerprint) derivation = bip32.parse_path(k.derivation) break # remove unnecessary derivations from inputs and outputs for inp in qr_psbt.inputs + qr_psbt.outputs: # keep only one derivation path (idealy ours) found = False pubkeys = list(inp.bip32_derivations.keys()) for i, pub in enumerate(pubkeys): if fgp and inp.bip32_derivations[pub].fingerprint != fgp: # only pop if we already saw our derivation # or if it's not the last one if found or i < len(pubkeys) - 1: inp.bip32_derivations.pop(k, None) else: found = True # remove scripts from outputs (DIY should know about the wallet) for out in qr_psbt.outputs: out.witness_script = None out.redeem_script = None # remove partial sigs from inputs for inp in qr_psbt.inputs: inp.partial_sigs = {} psbts["qrcode"] = qr_psbt.to_string() # we can add xpubs to SD card, but non_witness can be too large for MCU psbts["sdcard"] = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=True, taproot_derivations=True) psbts["hwi"] = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=True, taproot_derivations=True) return psbts
def walletcreatefundedpsbt( self, inputs, outputs, locktime=0, options={}, *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, changeAddresses} 5. bip32 derivations 6. solving data 7. blind = True - Specter-LiquidRPC specific thing - blind transaction after creation """ options = copy.deepcopy(options) change_addresses = ( options.pop("changeAddresses") if "changeAddresses" in options else None ) destinations = [] for o in outputs: for k in o: if k != "asset": destinations.append(addr_decode(k)[0]) res = super().__getattr__("walletcreatefundedpsbt")( inputs, outputs, locktime, options, *args, **kwargs ) psbt = res.get("psbt", None) # remove zero-output (bug in Elements) # TODO: remove after release if psbt: try: tx = PSET.from_string(psbt) # check if there are zero outputs has_zero = len([out for out in tx.outputs if out.value == 0]) > 0 has_blinded = any([out.blinding_pubkey for out in tx.outputs]) logger.error(has_zer, has_blinded) if has_blinded and has_zero: tx.outputs = [out for out in tx.outputs if out.value > 0] psbt = str(tx) res["psbt"] = psbt except: pass # replace change addresses from the transactions if we can if change_addresses and psbt: try: tx = PSET.from_string(psbt) cur = 0 for out in tx.outputs: # fee if out.script_pubkey.data == b"": continue # not change for sure if not out.bip32_derivations: continue # do change replacement if out.script_pubkey not in destinations: sc, bkey = addr_decode(change_addresses[cur]) cur += 1 out.script_pubkey = sc out.blinding_pubkey = bkey.sec() if bkey else None out.bip32_derivations = {} out.redeem_script = None out.witness_script = None # fill derivation info patched = ( super().__getattr__("walletprocesspsbt")(str(tx), False).get("psbt") ) patchedtx = PSET.from_string(patched) assert len(tx.outputs) == len(patchedtx.outputs) for out1, out2 in zip(tx.outputs, patchedtx.outputs): # fee if out1.script_pubkey.data == b"": continue # not change for sure if not out2.bip32_derivations: continue # do change replacement if out1.script_pubkey not in destinations: out1.bip32_derivations = out2.bip32_derivations out1.redeem_script = out2.redeem_script out1.witness_script = out2.witness_script res["psbt"] = str(tx) except Exception as e: logger.error(e) raise e 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) changepos = res.get("changepos", None) # no change output if changepos < 0: changepos = None # generate all blinding stuff ourselves in deterministic way tx.unblind( self.master_blinding_key ) # get values and blinding factors for inputs seed = tagged_hash("liquid/blinding_seed", self.master_blinding_key.secret) try: tx.blind(seed) # generate all blinding factors etc # proprietary fields for Specter - 00 is global blinding seed tx.unknown[b"\xfc\x07specter\x00"] = seed except PSBTError: seed = None # reblind and encode nonces in change output if seed and 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