def make_sign_and_push(ins_full, wallet, amount, output_addr=None, change_addr=None, hashcode=btc.SIGHASH_ALL, estimate_fee = False): """Utility function for easily building transactions from wallets """ total = sum(x['value'] for x in ins_full.values()) ins = list(ins_full.keys()) #random output address and change addr output_addr = wallet.get_new_addr(1, 1) if not output_addr else output_addr change_addr = wallet.get_new_addr(1, 0) if not change_addr else change_addr fee_est = estimate_tx_fee(len(ins), 2) if estimate_fee else 10000 outs = [{'value': amount, 'address': output_addr}, {'value': total - amount - fee_est, 'address': change_addr}] de_tx = btc.deserialize(btc.mktx(ins, outs)) scripts = {} for index, ins in enumerate(de_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) script = wallet.addr_to_script(ins_full[utxo]['address']) scripts[index] = (script, ins_full[utxo]['value']) binarize_tx(de_tx) de_tx = wallet.sign_tx(de_tx, scripts, hashcode=hashcode) #pushtx returns False on any error tx = binascii.hexlify(btc.serialize(de_tx)).decode('ascii') push_succeed = jm_single().bc_interface.pushtx(tx) if push_succeed: return btc.txhash(tx) else: return False
def add_tx_notify(self, txd, unconfirmfun, confirmfun, spentfun, notifyaddr, timeoutfun=None): if not self.notifythread: self.notifythread = BitcoinCoreNotifyThread(self) self.notifythread.start() one_addr_imported = False for outs in txd['outs']: addr = btc.script_to_address(outs['script'], get_p2pk_vbyte()) if self.rpc('getaccount', [addr]) != '': one_addr_imported = True break if not one_addr_imported: self.rpc('importaddress', [notifyaddr, 'joinmarket-notify', False]) tx_output_set = set([(sv['script'], sv['value']) for sv in txd['outs']]) self.txnotify_fun.append( (btc.txhash(btc.serialize(txd)), tx_output_set, unconfirmfun, confirmfun, spentfun, timeoutfun, False)) #create unconfirm timeout here, create confirm timeout in the other thread if timeoutfun: threading.Timer(cs_single().config.getint('TIMEOUT', 'unconfirm_timeout_sec'), bitcoincore_timeout_callback, args=(False, tx_output_set, self.txnotify_fun, timeoutfun)).start()
def sign_transaction(cls, tx, index, privkey, *args, **kwargs): hashcode = kwargs.get('hashcode') or btc.SIGHASH_ALL return btc.sign(btc.serialize(tx), index, privkey, hashcode=hashcode, amount=None, native=False)
def make_sign_and_push(ins_full, wallet_service, amount, output_addr=None, change_addr=None, hashcode=btc.SIGHASH_ALL, estimate_fee=False): """Utility function for easily building transactions from wallets """ assert isinstance(wallet_service, WalletService) total = sum(x['value'] for x in ins_full.values()) ins = list(ins_full.keys()) #random output address and change addr output_addr = wallet_service.get_new_addr( 1, 1) if not output_addr else output_addr change_addr = wallet_service.get_new_addr( 0, 1) if not change_addr else change_addr fee_est = estimate_tx_fee(len(ins), 2) if estimate_fee else 10000 outs = [{ 'value': amount, 'address': output_addr }, { 'value': total - amount - fee_est, 'address': change_addr }] de_tx = btc.deserialize(btc.mktx(ins, outs)) scripts = {} for index, ins in enumerate(de_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) script = wallet_service.addr_to_script(ins_full[utxo]['address']) scripts[index] = (script, ins_full[utxo]['value']) binarize_tx(de_tx) de_tx = wallet_service.sign_tx(de_tx, scripts, hashcode=hashcode) #pushtx returns False on any error push_succeed = jm_single().bc_interface.pushtx(btc.serialize(de_tx)) if push_succeed: txid = btc.txhash(btc.serialize(de_tx)) # in normal operation this happens automatically # but in some tests there is no monitoring loop: wallet_service.process_new_tx(de_tx, txid) return txid else: return False
def test_segwit_valid_txs(setup_segwit): with open("test/tx_segwit_valid.json", "r") as f: json_data = f.read() valid_txs = json.loads(json_data) for j in valid_txs: if len(j) < 2: continue deserialized_tx = btc.deserialize(str(j[1])) print pformat(deserialized_tx) assert btc.serialize(deserialized_tx) == str(j[1])
def outputs_watcher(self, wallet_name, notifyaddr, tx_output_set, unconfirmfun, confirmfun, timeoutfun): """Given a key for the watcher loop (notifyaddr), a wallet name (label), a set of outputs, and unconfirm, confirm and timeout callbacks, check to see if a transaction matching that output set has appeared in the wallet. Call the callbacks and update the watcher loop state. End the loop when the confirmation has been seen (no spent monitoring here). """ wl = self.tx_watcher_loops[notifyaddr] txlist = self.rpc("listtransactions", ["*", 100, 0, True]) for tx in txlist[::-1]: #changed syntax in 0.14.0; allow both syntaxes try: res = self.rpc("gettransaction", [tx["txid"], True]) except: try: res = self.rpc("gettransaction", [tx["txid"], 1]) except JsonRpcError as e: #This should never happen (gettransaction is a wallet rpc). log.warn("Failed gettransaction call; JsonRpcError") res = None except Exception as e: log.warn("Failed gettransaction call; unexpected error:") log.warn(str(e)) res = None if not res: continue if "confirmations" not in res: log.debug("Malformed gettx result: " + str(res)) return txd = self.get_deser_from_gettransaction(res) if txd is None: continue txos = set([(sv['script'], sv['value']) for sv in txd['outs']]) if not txos == tx_output_set: continue #Here we have found a matching transaction in the wallet. real_txid = btc.txhash(btc.serialize(txd)) if not wl[1] and res["confirmations"] == 0: log.debug("Tx: " + str(real_txid) + " seen on network.") unconfirmfun(txd, real_txid) wl[1] = True return if not wl[2] and res["confirmations"] > 0: log.debug("Tx: " + str(real_txid) + " has " + str(res["confirmations"]) + " confirmations.") confirmfun(txd, real_txid, res["confirmations"]) wl[2] = True wl[0].stop() return if res["confirmations"] < 0: log.debug("Tx: " + str(real_txid) + " has a conflict. Abandoning.") wl[0].stop() return
def sign_transaction(cls, tx, index, privkey, amount, hashcode=btc.SIGHASH_ALL, **kwargs): assert amount is not None return btc.sign(btc.serialize(tx), index, privkey, hashcode=hashcode, amount=amount, native=True)
def test_signing_simple(setup_wallet, wallet_cls, type_check): jm_single().config.set('BLOCKCHAIN', 'network', 'testnet') storage = VolatileStorage() wallet_cls.initialize(storage, get_network()) wallet = wallet_cls(storage) utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(0)) tx = btc.deserialize(btc.mktx(['{}:{}'.format(hexlify(utxo[0]).decode('ascii'), utxo[1])], ['00'*17 + ':' + str(10**8 - 9000)])) binarize_tx(tx) script = wallet.get_script(0, 1, 0) wallet.sign_tx(tx, {0: (script, 10**8)}) type_check(tx) txout = jm_single().bc_interface.pushtx(hexlify(btc.serialize(tx)).decode('ascii')) assert txout
def test_signing_simple(setup_wallet, wallet_cls, type_check): jm_single().config.set('BLOCKCHAIN', 'network', 'testnet') storage = VolatileStorage() wallet_cls.initialize(storage, get_network()) wallet = wallet_cls(storage) utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(0)) # The dummy output is constructed as an unspendable p2sh: tx = btc.deserialize(btc.mktx(['{}:{}'.format( hexlify(utxo[0]).decode('ascii'), utxo[1])], [btc.p2sh_scriptaddr(b"\x00",magicbyte=196) + ':' + str(10**8 - 9000)])) script = wallet.get_script(0, 1, 0) tx = wallet.sign_tx(tx, {0: (script, 10**8)}) type_check(tx) txout = jm_single().bc_interface.pushtx(btc.serialize(tx)) assert txout
def tx_watcher(self, txd, unconfirmfun, confirmfun, spentfun, c, n): """Called at a polling interval, checks if the given deserialized transaction (which must be fully signed) is (a) broadcast, (b) confirmed and (c) spent from. (c, n ignored in electrum version, just supports registering first confirmation). TODO: There is no handling of conflicts here. """ txid = btc.txhash(btc.serialize(txd)) wl = self.tx_watcher_loops[txid] #first check if in mempool (unconfirmed) #choose an output address for the query. Filter out #p2pkh addresses, assume p2sh (thus would fail to find tx on #some nonstandard script type) addr = None for i in range(len(txd['outs'])): if not btc.is_p2pkh_script(txd['outs'][i]['script']): addr = btc.script_to_address(txd['outs'][i]['script'], get_p2sh_vbyte()) break if not addr: log.error("Failed to find any p2sh output, cannot be a standard " "joinmarket transaction, fatal error!") reactor.stop() return unconftxs_res = self.get_from_electrum( 'blockchain.address.get_mempool', addr, blocking=True).get('result') unconftxs = [str(t['tx_hash']) for t in unconftxs_res] if not wl[1] and txid in unconftxs: jmprint("Tx: " + str(txid) + " seen on network.", "info") unconfirmfun(txd, txid) wl[1] = True return conftx = self.get_from_electrum('blockchain.address.listunspent', addr, blocking=True).get('result') conftxs = [str(t['tx_hash']) for t in conftx] if not wl[2] and len(conftxs) and txid in conftxs: jmprint("Tx: " + str(txid) + " is confirmed.", "info") confirmfun(txd, txid, 1) wl[2] = True #Note we do not stop the monitoring loop when #confirmations occur, since we are also monitoring for spending. return if not spentfun or wl[3]: return
def test_signing_imported(setup_wallet, wif, keytype, type_check): jm_single().config.set('BLOCKCHAIN', 'network', 'testnet') storage = VolatileStorage() SegwitLegacyWallet.initialize(storage, get_network()) wallet = SegwitLegacyWallet(storage) MIXDEPTH = 0 path = wallet.import_private_key(MIXDEPTH, wif, keytype) utxo = fund_wallet_addr(wallet, wallet.get_addr_path(path)) tx = btc.deserialize( btc.mktx(['{}:{}'.format(hexlify(utxo[0]).decode('ascii'), utxo[1])], ['00' * 17 + ':' + str(10**8 - 9000)])) script = wallet.get_script_path(path) tx = wallet.sign_tx(tx, {0: (script, 10**8)}) type_check(tx) txout = jm_single().bc_interface.pushtx(btc.serialize(tx)) assert txout
def test_signing_simple(setup_wallet, wallet_cls, type_check): jm_single().config.set('BLOCKCHAIN', 'network', 'testnet') storage = VolatileStorage() wallet_cls.initialize(storage, get_network()) wallet = wallet_cls(storage) utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(0)) # The dummy output is of length 25 bytes, because, for SegwitWallet, we else # trigger the tx-size-small DOS limit in Bitcoin Core (82 bytes is the # smallest "normal" transaction size (non-segwit size, ie no witness) tx = btc.deserialize( btc.mktx(['{}:{}'.format(hexlify(utxo[0]).decode('ascii'), utxo[1])], ['00' * 25 + ':' + str(10**8 - 9000)])) script = wallet.get_script(0, 1, 0) tx = wallet.sign_tx(tx, {0: (script, 10**8)}) type_check(tx) txout = jm_single().bc_interface.pushtx(btc.serialize(tx)) assert txout
def sign_transaction(cls, tx, index, privkey_locktime, amount, hashcode=btc.SIGHASH_ALL, **kwargs): assert amount is not None privkey, locktime = privkey_locktime privkey = hexlify(privkey).decode() pubkey = btc.privkey_to_pubkey(privkey) pubkey = unhexlify(pubkey) redeem_script = cls.pubkey_to_script_code((pubkey, locktime)) tx = btc.serialize(tx) sig = btc.get_p2sh_signature(tx, index, redeem_script, privkey, amount) return btc.apply_freeze_signature(tx, index, redeem_script, sig)
def test_serialization_roundtrip2(): #Data extracted from: #https://github.com/bitcoin/bitcoin/blob/master/src/test/data/tx_valid.json #These are a variety of rather strange edge case transactions, which are #still valid. #Note that of course this is only a serialization, not validity test, so #only currently of very limited significance with open(os.path.join(testdir, "tx_valid.json"), "r") as f: json_data = f.read() valid_txs = json.loads(json_data) for j in valid_txs: #ignore comment entries if len(j) < 2: continue print j deserialized = btc.deserialize(str(j[0])) print deserialized assert j[0] == btc.serialize(deserialized)
def test_signing_imported(setup_wallet, wif, keytype, type_check): jm_single().config.set('BLOCKCHAIN', 'network', 'testnet') storage = VolatileStorage() SegwitLegacyWallet.initialize(storage, get_network()) wallet = SegwitLegacyWallet(storage) MIXDEPTH = 0 path = wallet.import_private_key(MIXDEPTH, wif, keytype) utxo = fund_wallet_addr(wallet, wallet.get_address_from_path(path)) # The dummy output is constructed as an unspendable p2sh: tx = btc.deserialize(btc.mktx(['{}:{}'.format( hexlify(utxo[0]).decode('ascii'), utxo[1])], [btc.p2sh_scriptaddr(b"\x00",magicbyte=196) + ':' + str(10**8 - 9000)])) script = wallet.get_script_from_path(path) tx = wallet.sign_tx(tx, {0: (script, 10**8)}) type_check(tx) txout = jm_single().bc_interface.pushtx(btc.serialize(tx)) assert txout
def test_timelocked_output_signing(setup_wallet): jm_single().config.set('BLOCKCHAIN', 'network', 'testnet') ensure_bip65_activated() storage = VolatileStorage() SegwitLegacyWalletFidelityBonds.initialize(storage, get_network()) wallet = SegwitLegacyWalletFidelityBonds(storage) index = 0 timenumber = 0 script = wallet.get_script_and_update_map( FidelityBondMixin.FIDELITY_BOND_MIXDEPTH, FidelityBondMixin.BIP32_TIMELOCK_ID, index, timenumber) utxo = fund_wallet_addr(wallet, wallet.script_to_addr(script)) timestamp = wallet._time_number_to_timestamp(timenumber) tx = btc.deserialize(btc.mktx(['{}:{}'.format( hexlify(utxo[0]).decode('ascii'), utxo[1])], [btc.p2sh_scriptaddr(b"\x00",magicbyte=196) + ':' + str(10**8 - 9000)], locktime=timestamp+1)) tx = wallet.sign_tx(tx, {0: (script, 10**8)}) txout = jm_single().bc_interface.pushtx(btc.serialize(tx)) assert txout
def attach_signatures(self): """Once all signatures are available, they can be attached to construct a "fully_signed_tx" form of the transaction ready for broadcast (as distinct from the "base_form" without any signatures attached). """ assert self.fully_signed() self.fully_signed_tx = copy.deepcopy(self.base_form) for idx in range(len(self.ins)): tp = self.template.ins[idx].spk_type assert tp in ["NN", "p2sh-p2wpkh"] if tp == "NN": self.fully_signed_tx = btc.apply_p2wsh_multisignatures( self.fully_signed_tx, idx, self.signing_redeem_scripts[idx], self.signatures[idx]) else: k = self.keys["ins"][idx][self.keys["ins"][idx].keys()[0]] dtx = btc.deserialize(self.fully_signed_tx) dtx["ins"][idx][ "script"] = "16" + btc.pubkey_to_p2sh_p2wpkh_script(k) dtx["ins"][idx]["txinwitness"] = [self.signatures[idx][0], k] self.fully_signed_tx = btc.serialize(dtx)
def on_JM_TX_RECEIVED(self, nick, txhex, offer): # "none" flags p2ep protocol; pass through to the generic # on_tx handler for that: if offer == "none": return self.on_p2ep_tx_received(nick, txhex) offer = json.loads(offer) retval = self.client.on_tx_received(nick, txhex, offer) if not retval[0]: jlog.info("Maker refuses to continue on receipt of tx") else: sigs = retval[1] self.finalized_offers[nick] = offer tx = btc.deserialize(txhex) self.finalized_offers[nick]["txd"] = tx txid = btc.txhash(btc.serialize(tx)) # we index the callback by the out-set of the transaction, # because the txid is not known until all scriptSigs collected # (hence this is required for Makers, but not Takers). # For more info see WalletService.transaction_monitor(): txinfo = tuple((x["script"], x["value"]) for x in tx["outs"]) self.client.wallet_service.register_callbacks( [self.unconfirm_callback], txinfo, "unconfirmed") self.client.wallet_service.register_callbacks( [self.confirm_callback], txinfo, "confirmed") task.deferLater( reactor, float(jm_single().config.getint("TIMEOUT", "unconfirm_timeout_sec")), self.client.wallet_service.check_callback_called, txinfo, self.unconfirm_callback, "unconfirmed", "transaction with outputs: " + str(txinfo) + " not broadcast.") d = self.callRemote(commands.JMTXSigs, nick=nick, sigs=json.dumps(sigs)) self.defaultCallbacks(d) return {"accepted": True}
def mktx(self): """First, construct input and output lists as for a normal transaction construction, using the OCCTemplateTx corresponding inputs and outputs as information. To do this completely requires txids for all inputs. Thus, this must be called for this OCCTx *after* it has been called for all parent txs. We ensure that the txid for this Tx is set here, and is attached to all the Outpoint objects for its outputs. """ self.build_ins_from_template() self.build_outs_from_template() assert all([self.ins, self.outs]) self.base_form = btc.mktx([x[0] for x in self.ins], self.outs) dtx = btc.deserialize(self.base_form) if self.locktime: dtx["ins"][0]["sequence"] = 0 dtx["locktime"] = self.locktime #To set the txid, it's required that we set the #scriptSig and scriptPubkey objects. We don't yet #need to flag it segwit (we're not yet attaching #signatures) since we want txid not wtxid and the #former doesn't use segwit formatting anyway. for i, inp in enumerate(dtx["ins"]): sti = self.template.ins[i] if sti.spk_type == "p2sh-p2wpkh": inp["script"] = "16" + btc.pubkey_to_p2sh_p2wpkh_script( self.keys["ins"][i][sti.counterparty]) elif sti.spk_type == "NN": inp["script"] = "" self.txid = btc.txhash(btc.serialize(dtx)) #by setting the txid of the outpoints, we allow child #transactions to know the outpoint references for their inputs. for to in self.template.outs: to.txid = self.txid
def direct_send(wallet, amount, mixdepth, destaddr, answeryes=False, accept_callback=None, info_callback=None): """Send coins directly from one mixdepth to one destination address; does not need IRC. Sweep as for normal sendpayment (set amount=0). If answeryes is True, callback/command line query is not performed. If accept_callback is None, command line input for acceptance is assumed, else this callback is called: accept_callback: ==== args: deserialized tx, destination address, amount in satoshis, fee in satoshis returns: True if accepted, False if not ==== The info_callback takes one parameter, the information message (when tx is pushed), and returns nothing. This function returns: The txid if transaction is pushed, False otherwise """ #Sanity checks assert validate_address(destaddr)[0] assert isinstance(mixdepth, numbers.Integral) assert mixdepth >= 0 assert isinstance(amount, numbers.Integral) assert amount >= 0 assert isinstance(wallet, BaseWallet) from pprint import pformat txtype = wallet.get_txtype() if amount == 0: utxos = wallet.get_utxos_by_mixdepth()[mixdepth] if utxos == {}: log.error("There are no utxos in mixdepth: " + str(mixdepth) + ", quitting.") return total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype) outs = [{"address": destaddr, "value": total_inputs_val - fee_est}] else: #8 inputs to be conservative initial_fee_est = estimate_tx_fee(8, 2, txtype=txtype) utxos = wallet.select_utxos(mixdepth, amount + initial_fee_est) if len(utxos) < 8: fee_est = estimate_tx_fee(len(utxos), 2, txtype=txtype) else: fee_est = initial_fee_est total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) changeval = total_inputs_val - fee_est - amount outs = [{"value": amount, "address": destaddr}] change_addr = wallet.get_internal_addr(mixdepth) import_new_addresses(wallet, [change_addr]) outs.append({"value": changeval, "address": change_addr}) #Now ready to construct transaction log.info("Using a fee of : " + str(fee_est) + " satoshis.") if amount != 0: log.info("Using a change value of: " + str(changeval) + " satoshis.") txsigned = sign_tx(wallet, mktx(list(utxos.keys()), outs), utxos) log.info("Got signed transaction:\n") log.info(pformat(txsigned)) tx = serialize(txsigned) log.info("In serialized form (for copy-paste):") log.info(tx) actual_amount = amount if amount != 0 else total_inputs_val - fee_est log.info("Sends: " + str(actual_amount) + " satoshis to address: " + destaddr) if not answeryes: if not accept_callback: if input( 'Would you like to push to the network? (y/n):')[0] != 'y': log.info( "You chose not to broadcast the transaction, quitting.") return False else: accepted = accept_callback(pformat(txsigned), destaddr, actual_amount, fee_est) if not accepted: return False jm_single().bc_interface.pushtx(tx) txid = txhash(tx) successmsg = "Transaction sent: " + txid cb = log.info if not info_callback else info_callback cb(successmsg) return txid
def main(): # sets up grpc connection to lnd channel = get_secure_channel() # note that the 'admin' macaroon already has the required # permissions for the walletkit request, so we don't need # that third macaroon. macaroon, signer_macaroon = get_macaroons(["admin", "signer"]) # the main stub allows access to the default rpc commands: stub = lnrpc.LightningStub(channel) # the signer stub allows us to access the rpc for signing # transactions on our coins: stub_signer = signrpc.SignerStub(channel) # we also need a stub for the walletkit rpc to extract # public keys for addresses holding coins: stub_walletkit = walletrpc.WalletKitStub(channel) # Here we start the process to sign a custom tx. # 1. List unspent coins, get most recent ones (just an example). # 2. Get the pubkeys of those addresses. # 3. Get the next unused address in the wallet as destination. # 4. Build a transaction, (in future: optionally taking extra # inputs and outputs from elsewhere). # 5. Use signOutputRaw rpc to sign the new transaction. # 6. Use the walletkit PublishTransaction to publish. # Just an example of retrieving basic info, not necessary: # Retrieve and display the wallet balance response = stub.WalletBalance(ln.WalletBalanceRequest(), metadata=[('macaroon', macaroon)]) print("Current on-chain wallet balance: ", response.total_balance) inputs = get_our_coins(stub, macaroon) + get_other_coins() for inp in inputs: # Attach auxiliary data needed to the inputs, for signing. # Get the public key of an address inp["pubkey"] = stub_walletkit.KeyForAddress( walletkit.KeyForAddressRequest(addr_in=inp["utxo"].address), metadata=[('macaroon', macaroon)]).raw_key_bytes # this data (known as scriptCode in BIP143 parlance) # is the pubkeyhash script for this p2wpkh, as is needed # to construct the signature hash. # **NOTE** This code currently works with bech32 only. # TODO update to allow p2sh-p2wpkh in wallet coins, also. inp["script"] = btc.pubkey_to_p2pkh_script(inp["pubkey"]) # We need an output address for the transaction, this is taken from the # standard wallet 'new address' request (type 0 is bech32 p2wpkh): request = ln.NewAddressRequest(type=0, ) response = stub.NewAddress(request, metadata=[('macaroon', macaroon)]) output_address = response.address print("Generated new address: ", output_address) # Build the raw unsigned transaction tx_ins = [] output_amt = 0 for inp in inputs: tx_ins.append(inp["utxo"].outpoint.txid_str + ":" + str(inp["utxo"].outpoint.output_index)) output_amt += inp["utxo"].amount_sat fee_est = estimate_tx_fee(2, 1, "p2wpkh", 6, stub, macaroon) output = {"address": output_address, "value": output_amt - fee_est} tx_unsigned = btc.mktx(tx_ins, [output], version=2) print(btc.deserialize(tx_unsigned)) # use SignOutputRaw to sign each input (currently, they are all ours). raw_sigs = {} for i, inp in enumerate(inputs): # KeyDescriptors must contain at least one of the pubkey and the HD path, # here we use the latter: kd = signer.KeyDescriptor(raw_key_bytes=inp["pubkey"]) # specify the utxo information for this input into a TxOut: sdout = signer.TxOut(value=inp["utxo"].amount_sat, pk_script=unhexlify(inp["utxo"].pk_script)) # we must pass a list of SignDescriptors; we could batch all into # one grpc call if we preferred. The witnessscript field is # constructed above as the "script" field in the input dict. sds = [ signer.SignDescriptor(key_desc=kd, input_index=i, output=sdout, witness_script=inp["script"], sighash=1) ] req = signer.SignReq(raw_tx_bytes=unhexlify(tx_unsigned), sign_descs=sds) # here we make the actual signing request to lnd over grpc: response = stub_signer.SignOutputRaw(req, metadata=[('macaroon', signer_macaroon)]) # note that btcwallet's sign function does not return the sighash byte, # it must be added manually: raw_sigs[i] = response.raw_sigs[0] + sighash_all_bytes # insert the signatures into the relevant inputs in the deserialized tx tx_unsigned_deser = btc.deserialize(tx_unsigned) for i in range(len(inputs)): tx_unsigned_deser["ins"][i]["txinwitness"] = [ btc.safe_hexlify(raw_sigs[i]), btc.safe_hexlify(inputs[i]["pubkey"]) ] print("Signed transaction: \n", tx_unsigned_deser) hextx = btc.serialize(tx_unsigned_deser) print("Serialized: ", hextx) print("You can broadcast this externally e.g. via Bitcoin Core")
def on_tx_received(self, nick, txhex): """ Called when the sender-counterparty has sent a transaction proposal. 1. First we check for the expected destination and amount (this is sufficient to identify our cp, as this info was presumably passed out of band, as for any normal payment). 2. Then we verify the validity of the proposed non-coinjoin transaction; if not, reject, otherwise store this as a fallback transaction in case the protocol doesn't complete. 3. Next, we select utxos from our wallet, to add into the payment transaction as input. Try to select so as to not trigger the UIH2 condition, but continue (and inform user) even if we can't (if we can't select any coins, broadcast the non-coinjoin payment, if the user agrees). Proceeding with payjoin: 4. We update the output amount at the destination address. 5. We modify the change amount in the original proposal (which will be the only other output other than the destination), reducing it to account for the increased transaction fee caused by our additional proposed input(s). 6. Finally we sign our own input utxo(s) and re-serialize the tx, allowing it to be sent back to the counterparty. 7. If the transaction is not fully signed and broadcast within the time unconfirm_timeout_sec as specified in the joinmarket.cfg, we broadcast the non-coinjoin fallback tx instead. """ try: tx = btc.deserialize(txhex) except (IndexError, SerializationError, SerializationTruncationError) as e: return (False, 'malformed txhex. ' + repr(e)) self.user_info('obtained proposed fallback (non-coinjoin) ' +\ 'transaction from sender:\n' + pprint.pformat(tx)) if len(tx["outs"]) != 2: return (False, "Transaction has more than 2 outputs; not supported.") dest_found = False destination_index = -1 change_index = -1 proposed_change_value = 0 for index, out in enumerate(tx["outs"]): if out["script"] == btc.address_to_script(self.destination_addr): # we found the expected destination; is the amount correct? if not out["value"] == self.receiving_amount: return (False, "Wrong payout value in proposal from sender.") dest_found = True destination_index = index else: change_found = True proposed_change_out = out["script"] proposed_change_value = out["value"] change_index = index if not dest_found: return (False, "Our expected destination address was not found.") # Verify valid input utxos provided and check their value. # batch retrieval of utxo data utxo = {} ctr = 0 for index, ins in enumerate(tx['ins']): utxo_for_checking = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) utxo[ctr] = [index, utxo_for_checking] ctr += 1 utxo_data = jm_single().bc_interface.query_utxo_set( [x[1] for x in utxo.values()]) total_sender_input = 0 for i, u in iteritems(utxo): if utxo_data[i] is None: return (False, "Proposed transaction contains invalid utxos") total_sender_input += utxo_data[i]["value"] # Check that the transaction *as proposed* balances; check that the # included fee is within 0.3-3x our own current estimates, if not user # must decide. btc_fee = total_sender_input - self.receiving_amount - proposed_change_value self.user_info("Network transaction fee of fallback tx is: " + str(btc_fee) + " satoshis.") fee_est = estimate_tx_fee(len(tx['ins']), len(tx['outs']), txtype=self.wallet.get_txtype()) fee_ok = False if btc_fee > 0.3 * fee_est and btc_fee < 3 * fee_est: fee_ok = True else: if self.user_check("Is this transaction fee acceptable? (y/n):"): fee_ok = True if not fee_ok: return (False, "Proposed transaction fee not accepted due to tx fee: " + str(btc_fee)) # This direct rpc call currently assumes Core 0.17, so not using now. # It has the advantage of (a) being simpler and (b) allowing for any # non standard coins. # #res = jm_single().bc_interface.rpc('testmempoolaccept', [txhex]) #print("Got this result from rpc call: ", res) #if not res["accepted"]: # return (False, "Proposed transaction was rejected from mempool.") # Manual verification of the transaction signatures. Passing this # test does imply that the transaction is valid (unless there is # a double spend during the process), but is restricted to standard # types: p2pkh, p2wpkh, p2sh-p2wpkh only. Double spend is not counted # as a risk as this is a payment. for i, u in iteritems(utxo): if "txinwitness" in tx["ins"][u[0]]: ver_amt = utxo_data[i]["value"] try: ver_sig, ver_pub = tx["ins"][u[0]]["txinwitness"] except Exception as e: self.user_info("Segwit error: " + repr(e)) return (False, "Segwit input not of expected type, " "either p2sh-p2wpkh or p2wpkh") # note that the scriptCode is the same whether nested or not # also note that the scriptCode has to be inferred if we are # only given a transaction serialization. scriptCode = "76a914" + btc.hash160( unhexlify(ver_pub)) + "88ac" else: scriptCode = None ver_amt = None scriptSig = btc.deserialize_script(tx["ins"][u[0]]["script"]) if len(scriptSig) != 2: return ( False, "Proposed transaction contains unsupported input type") ver_sig, ver_pub = scriptSig if not btc.verify_tx_input(txhex, u[0], utxo_data[i]['script'], ver_sig, ver_pub, scriptCode=scriptCode, amount=ver_amt): return (False, "Proposed transaction is not correctly signed.") # At this point we are satisfied with the proposal. Record the fallback # in case the sender disappears and the payjoin tx doesn't happen: self.user_info( "We'll use this serialized transaction to broadcast if your" " counterparty fails to broadcast the payjoin version:") self.user_info(txhex) # Keep a local copy for broadcast fallback: self.fallback_tx = txhex # Now we add our own inputs: # See the gist comment here: # https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2799709 # which sets out the decision Bob must make. # In cases where Bob can add any amount, he selects one utxo # to keep it simple. # In cases where he must choose at least X, he selects one utxo # which provides X if possible, otherwise defaults to a normal # selection algorithm. # In those cases where he must choose X but X is unavailable, # he selects all coins, and proceeds anyway with payjoin, since # it has other advantages (CIOH and utxo defrag). my_utxos = {} largest_out = max(self.receiving_amount, proposed_change_value) max_sender_amt = max([u['value'] for u in utxo_data]) not_uih2 = False if max_sender_amt < largest_out: # just select one coin. # have some reasonable lower limit but otherwise choose # randomly; note that this is actually a great way of # sweeping dust ... self.user_info("Choosing one coin at random") try: my_utxos = self.wallet.select_utxos(self.mixdepth, jm_single().DUST_THRESHOLD, select_fn=select_one_utxo) except: return self.no_coins_fallback() not_uih2 = True else: # get an approximate required amount assuming 4 inputs, which is # fairly conservative (but guess by necessity). fee_for_select = estimate_tx_fee(len(tx['ins']) + 4, 2, txtype=self.wallet.get_txtype()) approx_sum = max_sender_amt - self.receiving_amount + fee_for_select try: my_utxos = self.wallet.select_utxos(self.mixdepth, approx_sum) not_uih2 = True except Exception: # TODO probably not logical to always sweep here. self.user_info("Sweeping all coins in this mixdepth.") my_utxos = self.wallet.get_utxos_by_mixdepth()[self.mixdepth] if my_utxos == {}: return self.no_coins_fallback() if not_uih2: self.user_info("The proposed tx does not trigger UIH2, which " "means it is indistinguishable from a normal " "payment. This is the ideal case. Continuing..") else: self.user_info("The proposed tx does trigger UIH2, which it makes " "it somewhat distinguishable from a normal payment," " but proceeding with payjoin..") my_total_in = sum([va['value'] for va in my_utxos.values()]) self.user_info("We selected inputs worth: " + str(my_total_in)) # adjust the output amount at the destination based on our contribution new_destination_amount = self.receiving_amount + my_total_in # estimate the required fee for the new version of the transaction total_ins = len(tx["ins"]) + len(my_utxos.keys()) est_fee = estimate_tx_fee(total_ins, 2, txtype=self.wallet.get_txtype()) self.user_info("We estimated a fee of: " + str(est_fee)) new_change_amount = total_sender_input + my_total_in - \ new_destination_amount - est_fee self.user_info("We calculated a new change amount of: " + str(new_change_amount)) self.user_info("We calculated a new destination amount of: " + str(new_destination_amount)) # now reconstruct the transaction with the new inputs and the # amount-changed outputs new_outs = [{ "address": self.destination_addr, "value": new_destination_amount }] if new_change_amount >= jm_single().BITCOIN_DUST_THRESHOLD: new_outs.append({ "script": proposed_change_out, "value": new_change_amount }) new_ins = [x[1] for x in utxo.values()] new_ins.extend(my_utxos.keys()) # set locktime for best anonset (Core, Electrum) - most recent block. # this call should never fail so no catch here. currentblock = jm_single().bc_interface.rpc("getblockchaininfo", [])["blocks"] new_tx = make_shuffled_tx(new_ins, new_outs, False, 2, currentblock) new_tx_deser = btc.deserialize(new_tx) # sign our inputs before transfer our_inputs = {} for index, ins in enumerate(new_tx_deser['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in my_utxos: continue script = self.wallet.addr_to_script(my_utxos[utxo]['address']) amount = my_utxos[utxo]['value'] our_inputs[index] = (script, amount) txs = self.wallet.sign_tx(btc.deserialize(new_tx), our_inputs) jm_single().bc_interface.add_tx_notify( txs, self.on_tx_unconfirmed, self.on_tx_confirmed, self.destination_addr, wallet_name=jm_single().bc_interface.get_wallet_name(self.wallet), txid_flag=False, vb=self.wallet._ENGINE.VBYTE) # The blockchain interface just abandons monitoring if the transaction # is not broadcast before the configured timeout; we want to take # action in this case, so we add an additional callback to the reactor: reactor.callLater( jm_single().config.getint("TIMEOUT", "unconfirm_timeout_sec"), self.broadcast_fallback) return (True, nick, btc.serialize(txs))
def test_spend_p2sh_p2wpkh_multi(setup_segwit, wallet_structure, in_amt, amount, segwit_amt, segwit_ins, o_ins): """Creates a wallet from which non-segwit inputs/ outputs can be created, constructs one or more p2wpkh in p2sh spendable utxos (by paying into the corresponding address) and tests spending them in combination. wallet_structure is in accordance with commontest.make_wallets, see docs there in_amt is the amount to pay into each address into the wallet (non-segwit adds) amount (in satoshis) is how much we will pay to the output address segwit_amt in BTC is the amount we will fund each new segwit address with segwit_ins is a list of input indices (where to place the funding segwit utxos) other_ins is a list of input indices (where to place the funding non-sw utxos) """ MIXDEPTH = 0 # set up wallets and inputs nsw_wallet = make_wallets(1, wallet_structure, in_amt, walletclass=LegacyWallet)[0]['wallet'] jm_single().bc_interface.sync_wallet(nsw_wallet, fast=True) sw_wallet = make_wallets(1, [[len(segwit_ins), 0, 0, 0, 0]], segwit_amt)[0]['wallet'] jm_single().bc_interface.sync_wallet(sw_wallet, fast=True) nsw_utxos = nsw_wallet.get_utxos_by_mixdepth_()[MIXDEPTH] sw_utxos = sw_wallet.get_utxos_by_mixdepth_()[MIXDEPTH] assert len(o_ins) <= len(nsw_utxos), "sync failed" assert len(segwit_ins) <= len(sw_utxos), "sync failed" total_amt_in_sat = 0 nsw_ins = {} for nsw_in_index in o_ins: total_amt_in_sat += in_amt * 10**8 nsw_ins[nsw_in_index] = nsw_utxos.popitem() sw_ins = {} for sw_in_index in segwit_ins: total_amt_in_sat += int(segwit_amt * 10**8) sw_ins[sw_in_index] = sw_utxos.popitem() all_ins = {} all_ins.update(nsw_ins) all_ins.update(sw_ins) # sanity checks assert len(all_ins) == len(nsw_ins) + len(sw_ins), \ "test broken, duplicate index" for k in all_ins: assert 0 <= k < len(all_ins), "test broken, missing input index" # FIXME: encoding mess, mktx should accept binary input formats tx_ins = [] for i, (txid, data) in sorted(all_ins.items(), key=lambda x: x[0]): tx_ins.append('{}:{}'.format(binascii.hexlify(txid[0]), txid[1])) # create outputs FEE = 50000 assert FEE < total_amt_in_sat - amount, "test broken, not enough funds" cj_script = nsw_wallet.get_new_script(MIXDEPTH + 1, True) change_script = nsw_wallet.get_new_script(MIXDEPTH, True) change_amt = total_amt_in_sat - amount - FEE tx_outs = [ {'script': binascii.hexlify(cj_script), 'value': amount}, {'script': binascii.hexlify(change_script), 'value': change_amt}] tx = btc.deserialize(btc.mktx(tx_ins, tx_outs)) binarize_tx(tx) # import new addresses to bitcoind jm_single().bc_interface.import_addresses( [nsw_wallet.script_to_addr(x) for x in [cj_script, change_script]], jm_single().bc_interface.get_wallet_name(nsw_wallet)) # sign tx scripts = {} for nsw_in_index in o_ins: inp = nsw_ins[nsw_in_index][1] scripts[nsw_in_index] = (inp['script'], inp['value']) nsw_wallet.sign_tx(tx, scripts) scripts = {} for sw_in_index in segwit_ins: inp = sw_ins[sw_in_index][1] scripts[sw_in_index] = (inp['script'], inp['value']) sw_wallet.sign_tx(tx, scripts) print(tx) # push and verify txid = jm_single().bc_interface.pushtx(binascii.hexlify(btc.serialize(tx))) assert txid balances = jm_single().bc_interface.get_received_by_addr( [nsw_wallet.script_to_addr(cj_script), nsw_wallet.script_to_addr(change_script)], None)['data'] assert balances[0]['balance'] == amount assert balances[1]['balance'] == change_amt
def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, accept_callback=None, info_callback=None): """Send coins directly from one mixdepth to one destination address; does not need IRC. Sweep as for normal sendpayment (set amount=0). If answeryes is True, callback/command line query is not performed. If accept_callback is None, command line input for acceptance is assumed, else this callback is called: accept_callback: ==== args: deserialized tx, destination address, amount in satoshis, fee in satoshis returns: True if accepted, False if not ==== The info_callback takes one parameter, the information message (when tx is pushed), and returns nothing. This function returns: The txid if transaction is pushed, False otherwise """ #Sanity checks assert validate_address(destination)[0] or is_burn_destination(destination) assert isinstance(mixdepth, numbers.Integral) assert mixdepth >= 0 assert isinstance(amount, numbers.Integral) assert amount >=0 assert isinstance(wallet_service.wallet, BaseWallet) if is_burn_destination(destination): #Additional checks if not isinstance(wallet_service.wallet, FidelityBondMixin): log.error("Only fidelity bond wallets can burn coins") return if answeryes: log.error("Burning coins not allowed without asking for confirmation") return if mixdepth != FidelityBondMixin.FIDELITY_BOND_MIXDEPTH: log.error("Burning coins only allowed from mixdepth " + str( FidelityBondMixin.FIDELITY_BOND_MIXDEPTH)) return if amount != 0: log.error("Only sweeping allowed when burning coins, to keep the tx " + "small. Tip: use the coin control feature to freeze utxos") return from pprint import pformat txtype = wallet_service.get_txtype() if amount == 0: utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth] if utxos == {}: log.error( "There are no available utxos in mixdepth: " + str(mixdepth) + ", quitting.") return total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) if is_burn_destination(destination): if len(utxos) > 1: log.error("Only one input allowed when burning coins, to keep " + "the tx small. Tip: use the coin control feature to freeze utxos") return address_type = FidelityBondMixin.BIP32_BURN_ID index = wallet_service.wallet.get_next_unused_index(mixdepth, address_type) path = wallet_service.wallet.get_path(mixdepth, address_type, index) privkey, engine = wallet_service.wallet._get_key_from_path(path) pubkey = engine.privkey_to_pubkey(privkey) pubkeyhash = bin_hash160(pubkey) #size of burn output is slightly different from regular outputs burn_script = mk_burn_script(pubkeyhash) #in hex fee_est = estimate_tx_fee(len(utxos), 0, txtype=txtype, extra_bytes=len(burn_script)/2) outs = [{"script": burn_script, "value": total_inputs_val - fee_est}] destination = "BURNER OUTPUT embedding pubkey at " \ + wallet_service.wallet.get_path_repr(path) \ + "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n" else: #regular send (non-burn) fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype) outs = [{"address": destination, "value": total_inputs_val - fee_est}] else: #8 inputs to be conservative initial_fee_est = estimate_tx_fee(8,2, txtype=txtype) utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est) if len(utxos) < 8: fee_est = estimate_tx_fee(len(utxos), 2, txtype=txtype) else: fee_est = initial_fee_est total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) changeval = total_inputs_val - fee_est - amount outs = [{"value": amount, "address": destination}] change_addr = wallet_service.get_internal_addr(mixdepth) outs.append({"value": changeval, "address": change_addr}) #compute transaction locktime, has special case for spending timelocked coins tx_locktime = compute_tx_locktime() if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \ isinstance(wallet_service.wallet, FidelityBondMixin): for outpoint, utxo in utxos.items(): path = wallet_service.script_to_path( wallet_service.addr_to_script(utxo["address"])) if not FidelityBondMixin.is_timelocked_path(path): continue path_locktime = path[-1] tx_locktime = max(tx_locktime, path_locktime+1) #compute_tx_locktime() gives a locktime in terms of block height #timelocked addresses use unix time instead #OP_CHECKLOCKTIMEVERIFY can only compare like with like, so we #must use unix time as the transaction locktime #Now ready to construct transaction log.info("Using a fee of : " + amount_to_str(fee_est) + ".") if amount != 0: log.info("Using a change value of: " + amount_to_str(changeval) + ".") txsigned = sign_tx(wallet_service, make_shuffled_tx( list(utxos.keys()), outs, False, 2, tx_locktime), utxos) log.info("Got signed transaction:\n") log.info(pformat(txsigned)) tx = serialize(txsigned) log.info("In serialized form (for copy-paste):") log.info(tx) actual_amount = amount if amount != 0 else total_inputs_val - fee_est log.info("Sends: " + amount_to_str(actual_amount) + " to destination: " + destination) if not answeryes: if not accept_callback: if input('Would you like to push to the network? (y/n):')[0] != 'y': log.info("You chose not to broadcast the transaction, quitting.") return False else: accepted = accept_callback(pformat(txsigned), destination, actual_amount, fee_est) if not accepted: return False jm_single().bc_interface.pushtx(tx) txid = txhash(tx) successmsg = "Transaction sent: " + txid cb = log.info if not info_callback else info_callback cb(successmsg) return txid
def graft_onto_single_acp(wallet, txhex, amount, destaddr): """Given a serialized txhex which is checked to be of form single|acp (one in, one out), a destination address and an amount to spend, grafts in this in-out pair (at index zero) to our own transaction spending amount amount to destination destaddr, and uses a user-specified transaction fee (normal joinmarket configuration), and sanity checks that the bump value is not greater than user specified bump option. Returned: serialized txhex of fully signed transaction. """ d = btc.deserialize(txhex) if len(d['ins']) != 1 or len(d['outs']) != 1: return (False, "Proposed tx should have 1 in 1 out, has: " + ','.join([str(len(d[x])) for x in ['ins', 'outs']])) #most important part: check provider hasn't bumped more than options.bump: other_utxo_in = d['ins'][0]['outpoint']['hash'] + ":" + str( d['ins'][0]['outpoint']['index']) res = jm_single().bc_interface.query_utxo_set(other_utxo_in) assert len(res) == 1 if not res[0]: return (False, "Utxo provided by counterparty not found.") excess = d['outs'][0]['value'] - res[0]["value"] if not excess <= options.bump: return (False, "Counterparty claims too much excess value: " + str(excess)) #Last sanity check - ensure that it's single|acp, else we're wasting our time try: if 'txinwitness' in d['ins'][0]: sig, pub = d['ins'][0]['txinwitness'] else: sig, pub = btc.deserialize_script(d['ins'][0]['script']) assert sig[-2:] == "83" except Exception as e: return ( False, "The transaction's signature does not parse as signed with " "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY, for p2pkh or p2sh-p2wpkh, or " "is otherwise invalid, and so is not valid for this function.\n" + repr(e)) #source inputs for our own chosen spending amount: try: input_utxos = wallet.select_utxos(options.mixdepth, amount) except Exception as e: return (False, "Unable to select sufficient coins from mixdepth: " + str(options.mixdepth)) total_selected = sum([x['value'] for x in input_utxos.values()]) fee = estimate_tx_fee(len(input_utxos) + 1, 3, txtype='p2sh-p2wpkh') change_amount = total_selected - amount - excess - fee changeaddr = wallet.get_new_addr(options.mixdepth, 1) #Build new transaction and, graft in signature ins = [other_utxo_in] + input_utxos.keys() outs = [ d['outs'][0], { 'address': destaddr, 'value': amount }, { 'address': changeaddr, 'value': change_amount } ] fulltx = btc.mktx(ins, outs) df = btc.deserialize(fulltx) #put back in original signature df['ins'][0]['script'] = d['ins'][0]['script'] if 'txinwitness' in d['ins'][0]: df['ins'][0]['txinwitness'] = d['ins'][0]['txinwitness'] fulltx = btc.serialize(df) for i, iu in enumerate(input_utxos): priv, inamt = get_privkey_amount_from_utxo(wallet, iu) print("Signing index: ", i + 1, " with privkey: ", priv, " and amount: ", inamt, " for utxo: ", iu) fulltx = btc.sign(fulltx, i + 1, priv, amount=inamt) return (True, fulltx)
def test_serialization_roundtrip(tx_type, tx_id, tx_hex): assert tx_hex == btc.serialize(btc.deserialize(tx_hex))
def tx_watcher(self, txd, unconfirmfun, confirmfun, spentfun, c, n): """Called at a polling interval, checks if the given deserialized transaction (which must be fully signed) is (a) broadcast, (b) confirmed and (c) spent from at index n, and notifies confirmation if number of confs = c. TODO: Deal with conflicts correctly. Here just abandons monitoring. """ txid = btc.txhash(btc.serialize(txd)) wl = self.tx_watcher_loops[txid] try: res = self.rpc('gettransaction', [txid, True]) except JsonRpcError as e: return if not res: return if "confirmations" not in res: log.debug("Malformed gettx result: " + str(res)) return if not wl[1] and res["confirmations"] == 0: log.debug("Tx: " + str(txid) + " seen on network.") unconfirmfun(txd, txid) wl[1] = True return if not wl[2] and res["confirmations"] > 0: log.debug("Tx: " + str(txid) + " has " + str(res["confirmations"]) + " confirmations.") confirmfun(txd, txid, res["confirmations"]) if c <= res["confirmations"]: wl[2] = True #Note we do not stop the monitoring loop when #confirmations occur, since we are also monitoring for spending. return if res["confirmations"] < 0: log.debug("Tx: " + str(txid) + " has a conflict. Abandoning.") wl[0].stop() return if not spentfun or wl[3]: return #To trigger the spent callback, we check if this utxo outpoint appears in #listunspent output with 0 or more confirmations. Note that this requires #we have added the destination address to the watch-only wallet, otherwise #that outpoint will not be returned by listunspent. res2 = self.rpc('listunspent', [0, 999999]) if not res2: return txunspent = False for r in res2: if "txid" not in r: continue if txid == r["txid"] and n == r["vout"]: txunspent = True break if not txunspent: #We need to find the transaction which spent this one; #assuming the address was added to the wallet, then this #transaction must be in the recent list retrieved via listunspent. #For each one, use gettransaction to check its inputs. #This is a bit expensive, but should only occur once. txlist = self.rpc("listtransactions", ["*", 1000, 0, True]) for tx in txlist[::-1]: #changed syntax in 0.14.0; allow both syntaxes try: res = self.rpc("gettransaction", [tx["txid"], True]) except: try: res = self.rpc("gettransaction", [tx["txid"], 1]) except: #This should never happen (gettransaction is a wallet rpc). log.info("Failed any gettransaction call") res = None if not res: continue deser = self.get_deser_from_gettransaction(res) if deser is None: continue for vin in deser["ins"]: if not "outpoint" in vin: #coinbases continue if vin["outpoint"]["hash"] == txid and vin["outpoint"][ "index"] == n: #recover the deserialized form of the spending transaction. log.info("We found a spending transaction: " + \ btc.txhash(binascii.unhexlify(res["hex"]))) res2 = self.rpc("gettransaction", [tx["txid"], True]) spending_deser = self.get_deser_from_gettransaction( res2) if not spending_deser: log.info( "ERROR: could not deserialize spending tx.") #Should never happen, it's a parsing bug. #No point continuing to monitor, we just hope we #can extract the secret by scanning blocks. wl[3] = True return spentfun(spending_deser, vin["outpoint"]["hash"]) wl[3] = True return
def add_tx_notify(self, txd, unconfirmfun, confirmfun, notifyaddr, wallet_name=None, timeoutfun=None, spentfun=None, txid_flag=True, n=0, c=1, vb=None): """Given a deserialized transaction txd, callback functions for broadcast and confirmation of the transaction, an address to import, and a callback function for timeout, set up a polling loop to check for events on the transaction. Also optionally set to trigger "confirmed" callback on number of confirmations c. Also checks for spending (if spentfun is not None) of the outpoint n. If txid_flag is True, we create a watcher loop on the txid (hence only really usable in a segwit context, and only on fully formed transactions), else we create a watcher loop on the output set of the transaction (taken from the outs field of the txd). """ if not vb: vb = get_p2pk_vbyte() if isinstance(self, BitcoinCoreInterface) or isinstance( self, RegtestBitcoinCoreInterface): #This code ensures that a walletnotify is triggered, by #ensuring that at least one of the output addresses is #imported into the wallet (note the sweep special case, where #none of the output addresses belong to me). one_addr_imported = False for outs in txd['outs']: addr = btc.script_to_address(outs['script'], vb) try: if self.is_address_imported(addr): one_addr_imported = True break except JsonRpcError as e: log.debug("Failed to getaccount for address: " + addr) log.debug("This is normal for bech32 addresses.") continue if not one_addr_imported: try: self.rpc('importaddress', [notifyaddr, 'joinmarket-notify', False]) except JsonRpcError as e: #In edge case of address already controlled #by another account, warn but do not quit in middle of tx. #Can occur if destination is owned in Core wallet. if e.code == -4 and e.message == "The wallet already " + \ "contains the private key for this address or script": log.warn("WARNING: Failed to import address: " + notifyaddr) #No other error should be possible else: raise #Warning! In case of txid_flag false, this is *not* a valid txid, #but only a hash of an incomplete transaction serialization. txid = btc.txhash(btc.serialize(txd)) if not txid_flag: tx_output_set = set([(sv['script'], sv['value']) for sv in txd['outs']]) loop = task.LoopingCall(self.outputs_watcher, wallet_name, notifyaddr, tx_output_set, unconfirmfun, confirmfun, timeoutfun) log.debug("Created watcher loop for address: " + notifyaddr) loopkey = notifyaddr else: loop = task.LoopingCall(self.tx_watcher, txd, unconfirmfun, confirmfun, spentfun, c, n) log.debug("Created watcher loop for txid: " + txid) loopkey = txid self.tx_watcher_loops[loopkey] = [loop, False, False, False] #Hardcoded polling interval, but in any case it can be very short. loop.start(5.0) #Give up on un-broadcast transactions and broadcast but not confirmed #transactions as per settings in the config. reactor.callLater( float(jm_single().config.get("TIMEOUT", "unconfirm_timeout_sec")), self.tx_network_timeout, loopkey) confirm_timeout_sec = int(jm_single().config.get( "TIMEOUT", "confirm_timeout_hours")) * 3600 reactor.callLater(confirm_timeout_sec, self.tx_timeout, txd, loopkey, timeoutfun)