def script_to_address(self, script): """Return the address for a given output script, which will be p2sh-p2wpkh for the segwit (currently). The underlying witness is however invisible at this layer; so it's just a p2sh address. """ return btc.script_to_address(script, get_p2sh_vbyte())
def push(self): tx = btc.serialize(self.latest_tx) jlog.debug('\n' + tx) self.txid = btc.txhash(tx) jlog.info('txid = ' + self.txid) #If we are sending to a bech32 address, in case of sweep, will #need to use that bech32 for address import, which requires #converting to script (Core does not allow import of bech32) if self.my_cj_addr.lower()[:2] in ['bc', 'tb']: notify_addr = btc.address_to_script(self.my_cj_addr) else: notify_addr = self.my_cj_addr #add the txnotify callbacks *before* pushing in case the #walletnotify is triggered before the notify callbacks are set up; #this does leave a dangling notify callback if the push fails, but #that doesn't cause problems. jm_single().bc_interface.add_tx_notify(self.latest_tx, self.unconfirm_callback, self.confirm_callback, notify_addr, vb=get_p2sh_vbyte()) tx_broadcast = jm_single().config.get('POLICY', 'tx_broadcast') nick_to_use = None if tx_broadcast == 'self': pushed = jm_single().bc_interface.pushtx(tx) elif tx_broadcast in ['random-peer', 'not-self']: n = len(self.maker_utxo_data) if tx_broadcast == 'random-peer': i = random.randrange(n + 1) else: i = random.randrange(n) if i == n: pushed = jm_single().bc_interface.pushtx(tx) else: nick_to_use = self.maker_utxo_data.keys()[i] pushed = True else: jlog.info("Only self, random-peer and not-self broadcast " "methods supported. Reverting to self-broadcast.") pushed = jm_single().bc_interface.pushtx(tx) if not pushed: self.on_finished_callback(False, fromtx=True) else: if nick_to_use: return (nick_to_use, tx)
def verify_unsigned_tx(self, txd, offerinfo): tx_utxo_set = set(ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) for ins in txd['ins']) utxos = offerinfo["utxos"] cjaddr = offerinfo["cjaddr"] changeaddr = offerinfo["changeaddr"] amount = offerinfo["amount"] cjfee = offerinfo["offer"]["cjfee"] txfee = offerinfo["offer"]["txfee"] ordertype = offerinfo["offer"]["ordertype"] my_utxo_set = set(utxos.keys()) if not tx_utxo_set.issuperset(my_utxo_set): return (False, 'my utxos are not contained') my_total_in = sum([va['value'] for va in utxos.values()]) real_cjfee = calc_cj_fee(ordertype, cjfee, amount) expected_change_value = (my_total_in - amount - txfee + real_cjfee) jlog.info('potentially earned = {}'.format(real_cjfee - txfee)) jlog.info('mycjaddr, mychange = {}, {}'.format(cjaddr, changeaddr)) times_seen_cj_addr = 0 times_seen_change_addr = 0 for outs in txd['outs']: addr = btc.script_to_address(outs['script'], get_p2sh_vbyte()) if addr == cjaddr: times_seen_cj_addr += 1 if outs['value'] != amount: return (False, 'Wrong cj_amount. I expect ' + str(amount)) if addr == changeaddr: times_seen_change_addr += 1 if outs['value'] != expected_change_value: return (False, 'wrong change, i expect ' + str( expected_change_value)) if times_seen_cj_addr != 1 or times_seen_change_addr != 1: fmt = ('cj or change addr not in tx ' 'outputs once, #cjaddr={}, #chaddr={}').format return (False, (fmt(times_seen_cj_addr, times_seen_change_addr))) return (True, None)
def test_net_byte(): load_test_config() assert struct.unpack(b'B', get_p2pk_vbyte())[0] == 0x6f assert struct.unpack(b'B', get_p2sh_vbyte())[0] == 196
def get_vbyte(self): return get_p2sh_vbyte()
def test_net_byte(): load_program_config() assert get_p2pk_vbyte() == 0x6f assert get_p2sh_vbyte() == 196
def receive_utxos(self, ioauth_data): """Triggered when the daemon returns utxo data from makers who responded; this is the completion of phase 1 of the protocol """ if self.aborted: return (False, "User aborted") rejected_counterparties = [] #Enough data, but need to authorize against the btc pubkey first. for nick, nickdata in ioauth_data.iteritems(): utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata if not self.auth_counterparty(btc_sig, auth_pub, maker_pk): jlog.debug( "Counterparty encryption verification failed, aborting: " + nick) #This counterparty must be rejected rejected_counterparties.append(nick) for rc in rejected_counterparties: del ioauth_data[rc] self.maker_utxo_data = {} for nick, nickdata in ioauth_data.iteritems(): utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata self.utxos[nick] = utxo_list utxo_data = jm_single().bc_interface.query_utxo_set( self.utxos[nick]) if None in utxo_data: jlog.warn(('ERROR outputs unconfirmed or already spent. ' 'utxo_data={}').format(pprint.pformat(utxo_data))) # when internal reviewing of makers is created, add it here to # immediately quit; currently, the timeout thread suffices. continue #Complete maker authorization: #Extract the address fields from the utxos #Construct the Bitcoin address for the auth_pub field #Ensure that at least one address from utxos corresponds. input_addresses = [d['address'] for d in utxo_data] auth_address = btc.pubkey_to_p2sh_p2wpkh_address( auth_pub, get_p2sh_vbyte()) if not auth_address in input_addresses: jlog.warn("ERROR maker's (" + nick + ")" " authorising pubkey is not included " "in the transaction: " + str(auth_address)) #this will not be added to the transaction, so we will have #to recheck if we have enough continue total_input = sum([d['value'] for d in utxo_data]) real_cjfee = calc_cj_fee(self.orderbook[nick]['ordertype'], self.orderbook[nick]['cjfee'], self.cjamount) change_amount = (total_input - self.cjamount - self.orderbook[nick]['txfee'] + real_cjfee) # certain malicious and/or incompetent liquidity providers send # inputs totalling less than the coinjoin amount! this leads to # a change output of zero satoshis; this counterparty must be removed. if change_amount < jm_single().DUST_THRESHOLD: fmt = ('ERROR counterparty requires sub-dust change. nick={}' 'totalin={:d} cjamount={:d} change={:d}').format jlog.warn(fmt(nick, total_input, self.cjamount, change_amount)) jlog.warn("Invalid change, too small, nick= " + nick) continue self.outputs.append({ 'address': change_addr, 'value': change_amount }) fmt = ('fee breakdown for {} totalin={:d} ' 'cjamount={:d} txfee={:d} realcjfee={:d}').format jlog.info( fmt(nick, total_input, self.cjamount, self.orderbook[nick]['txfee'], real_cjfee)) self.outputs.append({'address': cj_addr, 'value': self.cjamount}) self.cjfee_total += real_cjfee self.maker_txfee_contributions += self.orderbook[nick]['txfee'] self.maker_utxo_data[nick] = utxo_data #Apply business logic of how many counterparties are enough: if len(self.maker_utxo_data.keys()) < jm_single().config.getint( "POLICY", "minimum_makers"): self.taker_info_callback("INFO", "Not enough counterparties, aborting.") return (False, "Not enough counterparties responded to fill, giving up") self.taker_info_callback("INFO", "Got all parts, enough to build a tx") self.nonrespondants = list(self.maker_utxo_data.keys()) my_total_in = sum( [va['value'] for u, va in self.input_utxos.iteritems()]) if self.my_change_addr: #Estimate fee per choice of next/3/6 blocks targetting. estimated_fee = estimate_tx_fee(len(sum(self.utxos.values(), [])), len(self.outputs) + 2, txtype=self.wallet.get_txtype()) jlog.info("Based on initial guess: " + str(self.total_txfee) + ", we estimated a miner fee of: " + str(estimated_fee)) #reset total self.total_txfee = estimated_fee my_txfee = max(self.total_txfee - self.maker_txfee_contributions, 0) my_change_value = (my_total_in - self.cjamount - self.cjfee_total - my_txfee) #Since we could not predict the maker's inputs, we may end up needing #too much such that the change value is negative or small. Note that #we have tried to avoid this based on over-estimating the needed amount #in SendPayment.create_tx(), but it is still a possibility if one maker #uses a *lot* of inputs. if self.my_change_addr and my_change_value <= 0: raise ValueError("Calculated transaction fee of: " + str(self.total_txfee) + " is too large for our inputs;Please try again.") elif self.my_change_addr and my_change_value <= jm_single( ).BITCOIN_DUST_THRESHOLD: jlog.info("Dynamically calculated change lower than dust: " + str(my_change_value) + "; dropping.") self.my_change_addr = None my_change_value = 0 jlog.info( 'fee breakdown for me totalin=%d my_txfee=%d makers_txfee=%d cjfee_total=%d => changevalue=%d' % (my_total_in, my_txfee, self.maker_txfee_contributions, self.cjfee_total, my_change_value)) if self.my_change_addr is None: if my_change_value != 0 and abs(my_change_value) != 1: # seems you wont always get exactly zero because of integer # rounding so 1 satoshi extra or fewer being spent as miner # fees is acceptable jlog.info(('WARNING CHANGE NOT BEING ' 'USED\nCHANGEVALUE = {}').format(my_change_value)) else: self.outputs.append({ 'address': self.my_change_addr, 'value': my_change_value }) self.utxo_tx = [ dict([('output', u)]) for u in sum(self.utxos.values(), []) ] self.outputs.append({ 'address': self.coinjoin_address(), 'value': self.cjamount }) random.shuffle(self.utxo_tx) random.shuffle(self.outputs) tx = btc.mktx(self.utxo_tx, self.outputs) jlog.info('obtained tx\n' + pprint.pformat(btc.deserialize(tx))) self.latest_tx = btc.deserialize(tx) for index, ins in enumerate(self.latest_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in self.input_utxos.keys(): continue # placeholders required ins['script'] = 'deadbeef' self.taker_info_callback("INFO", "Built tx, sending to counterparties.") return (True, self.maker_utxo_data.keys(), tx)
def get_vbyte(): return get_p2sh_vbyte()