def handshake(self, d): """Check that the proposed coinswap parameters are acceptable. """ self.set_handshake_parameters() self.bbmb = self.wallet.get_balance_by_mixdepth(verbose=False) if d["coinswapcs_version"] != cs_single().CSCS_VERSION: return (False, "wrong CoinSwapCS version, was: " + \ str(d["coinswapcs_version"]) + ", should be: " + \ str(cs_single().CSCS_VERSION)) #Allow client to decide how long to wait, but within limits: if d["tx01_confirm_wait"] < 1 or d["tx01_confirm_wait"] > cs_single( ).config.getint("TIMEOUT","tx01_confirm_wait") + 2: return (False, "Mismatched tx01_confirm_wait, was: " + \ str(d["tx01_confirm_wait"]) + ", should be >=1 and less than:" + \ cs_single().config.get("TIMEOUT", "tx01_confirm_wait") + 3) if not self.coinswap_parameters.set_session_id(d["session_id"]): return (False, "invalid session id proposed: " + str(d["session_id"])) #immediately set the state file to the correct value self.state_file = self.state_file + self.coinswap_parameters.session_id + '.json' if d["source_chain"] != self.source_chain: return (False, "source chain was wrong: " + d["source_chain"]) if d["destination_chain"] != self.destination_chain: return (False, "destination chain was wrong: " + d["destination_chain"]) if d["amount"] < self.minimum_amount: return (False, "Requested amount too small: " + d["amount"]) if d["amount"] > self.maximum_amount: return (False, "Requested amount too large: " + d["amount"]) return (True, "Handshake parameters from Alice accepted")
def test_run_both(setup_wallets, runtype): #hack to account for the fact that Carol does not even run #if the handshake is bad; this is done to force the reactor to stop. if runtype == "badhandshake": cs_single().num_entities_running = 1 #The setup of each test case is the same; the only difference is the #participant classes (only Alice for now) ac = alice_classes[runtype] if runtype in alice_classes else None cc = carol_classes[runtype] if runtype in carol_classes else None fail_alice_state = alice_recover_cases[ runtype] if runtype in alice_recover_cases else None fail_carol_state = carol_recover_cases[ runtype] if runtype in carol_recover_cases else None alices, carol_bbmb, carol_wallet = runcase(ac, cc, fail_alice_state, fail_carol_state) #test case function will only return on reactor shutdown; Alice and Carol #objects are set at the start, but are references so updated. #Check the wallet states reflect the expected updates. #TODO handle multiple alices with different amounts against one Carol. if runtype == "badhandshake": for a in alices: a.bbma = a.wallet.get_balance_by_mixdepth(verbose=False) expected_spent = reasonable_fee_maximum*4 + cs_single( ).config.getint("SERVER", "minimum_coinswap_fee") if runtype in alice_funds_not_moved_cases: for i, alice in enumerate(alices): assert alice.bbmb[0] == alice.bbma[0] elif runtype in ["cooperative", "cbadreceivetx4sig", "ra11", "rc8", "rc9"]: #in all of these cases Alice's payment is complete for i, alice in enumerate(alices): funds_spent = alice.bbmb[0] - alice.bbma[0] funds_received = alice.bbma[1] - alice.bbmb[1] assert funds_spent - funds_received <= expected_spent + reasonable_fee_maximum else: #Ensure Alice did not pay too much and only spent back to 0 depth for i, alice in enumerate(alices): assert alice.bbma[1] == 0 funds_spent = alice.bbmb[0] - alice.bbma[0] assert funds_spent <= expected_spent #Carol is handled a bit differently, since Carol instances are initiated on #the fly, we instead query the wallet object directly for the final balances. sync_wallet(carol_wallet) carol_bbma = carol_wallet.get_balance_by_mixdepth(verbose=False) if runtype in carol_funds_not_moved_cases: assert carol_bbma[0] >= carol_bbmb[0] assert carol_bbma[0] - carol_bbmb[0] <= reasonable_fee_maximum + cs_single( ).config.getint("SERVER", "minimum_coinswap_fee") elif runtype in ["cooperative", "rc9"]: funds_spent = carol_bbmb[0] - carol_bbma[0] funds_received = carol_bbma[1] - carol_bbmb[1] assert funds_received - funds_spent >= cs_single( ).config.getint("SERVER", "minimum_coinswap_fee") - reasonable_fee_maximum else: #All cases of backout and funds have moved assert carol_bbmb[1] == 0 #Here we assert carol did not lose money; the alice checks are sufficient #to ensure carol didn't get too much assert carol_bbma[0] - carol_bbmb[0] > 0
def get_state_machine_callbacks(self): return [ (self.handshake, False, -1), (self.negotiate_coinswap_parameters, False, -1), (self.complete_negotiation, False, -1), (self.send_tx0id_hx_tx2sig, True, -1), (self.receive_txid1_tx23sig, False, -1), (self.send_tx3, True, -1), (self.broadcast_tx0, False, -1), (self.see_tx0_tx1, True, -1), #The timeout here is on waiting for confirmations, so very long (self.wait_for_phase_2, False, cs_single().one_confirm_timeout * cs_single().config.getint("TIMEOUT", "tx01_confirm_wait")), #only updates after confirmation; the custom delay here is to #account for network propagation delays for the TX0/TX1 conf. (self.send_coinswap_secret, False, cs_single().config.getint("TIMEOUT", "propagation_buffer")), (self.receive_tx5_sig, False, -1), #Give enough time for bitcoin network propagation here (self.broadcast_tx5, True, cs_single().config.getint("TIMEOUT", "propagation_buffer")), #this state only completes on confirmation of TX5. #We shouldn't really timeout here; honest behaviour means #always send the tx4 sig; hence crucial to pay good fees. (self.send_tx4_sig, False, cs_single().one_confirm_timeout) ]
def sync_unspent(self, wallet): from jmclient.wallet import BitcoinCoreWallet if isinstance(wallet, BitcoinCoreWallet): return st = time.time() wallet_name = self.get_wallet_name(wallet) wallet.unspent = {} listunspent_args = [] if 'listunspent_args' in cs_single().config.options('POLICY'): listunspent_args = ast.literal_eval(cs_single().config.get( 'POLICY', 'listunspent_args')) unspent_list = self.rpc('listunspent', listunspent_args) for u in unspent_list: if 'account' not in u: continue if u['account'] != wallet_name: continue if u['address'] not in wallet.addr_cache: continue wallet.unspent[u['txid'] + ':' + str(u['vout'])] = { 'address': u['address'], 'value': int(Decimal(str(u['amount'])) * Decimal('1e8')) } et = time.time() cslog.debug('bitcoind sync_unspent took ' + str((et - st)) + 'sec')
def handshake(self, alice_handshake): """Check that the proposed coinswap parameters are acceptable. """ self.set_handshake_parameters() self.bbmb = self.wallet.get_balance_by_mixdepth(verbose=False) try: d = alice_handshake[3] if d["coinswapcs_version"] != cs_single().CSCS_VERSION: return (False, "wrong CoinSwapCS version, was: " + \ str(d["coinswapcs_version"]) + ", should be: " + \ str(cs_single().CSCS_VERSION)) #Allow client to decide how long to wait, but within our range: tx01min, tx01max = [int(x) for x in cs_single().config.get( "SERVER", "tx01_confirm_range").split(",")] if not isinstance(d["tx01_confirm_wait"], int): return (False, "Invalid type confirm wait type (should be int)") if d["tx01_confirm_wait"] < tx01min or d["tx01_confirm_wait"] > tx01max: return (False, "Mismatched tx01_confirm_wait, was: " + str( d["tx01_confirm_wait"])) self.coinswap_parameters.set_tx01_confirm_wait(d["tx01_confirm_wait"]) self.sm.reset_timeouts([5, 6], cs_single().one_confirm_timeout * d[ "tx01_confirm_wait"]) if not "key_session" in d: #TODO validate that it's a real pubkey return (False, "no session key from Alice") if d["source_chain"] != self.source_chain: return (False, "source chain was wrong: " + d["source_chain"]) if d["destination_chain"] != self.destination_chain: return (False, "destination chain was wrong: " + d[ "destination_chain"]) if not isinstance(d["amount"], int): return (False, "Invalid amount type (should be int)") if d["amount"] < self.minimum_amount: return (False, "Requested amount too small: " + str(d["amount"])) if d["amount"] > self.maximum_amount: return (False, "Requested amount too large: " + str(d["amount"])) self.coinswap_parameters.set_base_amount(d["amount"]) if not isinstance(d["bitcoin_fee"], int): return (False, "Invalid type for bitcoin fee, should be int.") if d["bitcoin_fee"] < estimate_tx_fee((1, 2, 2), 1, txtype='p2shMofN')/2.0: return (False, "Suggested bitcoin transaction fee is too low.") if d["bitcoin_fee"] > estimate_tx_fee((1, 2, 2), 1, txtype='p2shMofN')*2.0: return (False, "Suggested bitcoin transaction fee is too high.") self.coinswap_parameters.set_bitcoin_fee(d["bitcoin_fee"]) #set the session pubkey for authorising future requests self.coinswap_parameters.set_pubkey("key_session", d["key_session"]) except Exception as e: return (False, "Error parsing handshake from counterparty, ignoring: " + \ repr(e)) return (self.coinswap_parameters.session_id, "Handshake parameters from Alice accepted")
def test_run_both(setup_wallets, runtype): #hack to account for the fact that Carol does not even run #if the handshake is bad; this is done to force the reactor to stop. if runtype == "badhandshake": cs_single().num_entities_running = 1 #The setup of each test case is the same; the only difference is the #participant classes (only Alice for now) ac = alice_classes[runtype] if runtype in alice_classes else None cc = carol_classes[runtype] if runtype in carol_classes else None alices, carol_bbmb, carol_wallet = runcase(ac, cc) #test case function will only return on reactor shutdown; Alice and Carol #objects are set at the start, but are references so updated. #Check the wallet states reflect the expected updates. #TODO handle multiple alices with different amounts against one Carol. expected_amt = amounts[0] - reasonable_fee_maximum if runtype in alice_funds_not_moved_cases: for i, alice in enumerate(alices): assert alice.bbmb[0] == alice.bbma[0] else: for i, alice in enumerate(alices): funds_spent = alice.bbmb[0] - alice.bbma[0] funds_received = alice.bbma[1] - alice.bbmb[1] assert_funds_balance(expected_amt, funds_spent, funds_received) #Carol is handled a bit differently, since Carol instances are initiated on #the fly, we instead query the wallet object directly for the final balances. sync_wallet(carol_wallet) carol_bbma = carol_wallet.get_balance_by_mixdepth(verbose=False) if runtype in carol_funds_not_moved_cases: assert carol_bbma[0] == carol_bbmb[0] else: funds_spent = carol_bbmb[0] - carol_bbma[0] funds_received = carol_bbma[1] - carol_bbmb[1] assert_funds_balance(expected_amt, funds_spent, funds_received)
def estimate_fee_per_kb(self, N): if not self.absurd_fees: return super(RegtestBitcoinCoreInterface, self).estimate_fee_per_kb(N) else: return cs_single().config.getint("POLICY", "absurd_fee_per_kb") + 100
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 redeem_tx3_with_lock(self): """Must be called after LOCK1, and TX3 must be broadcast but not-already-spent. Returns True if succeeds in broadcasting a redemption (to tx5_address), False otherwise. """ #**CONSTRUCT TX3-redeem-timeout; use a fresh address to redeem dest_addr = self.wallet.get_new_addr(0, 1, True) self.tx3redeem = CoinSwapRedeemTX23Timeout( self.coinswap_parameters.pubkeys["key_TX3_secret"], self.hashed_secret, self.coinswap_parameters.timeouts["LOCK1"], self.coinswap_parameters.pubkeys["key_TX3_lock"], self.tx3.txid + ":0", self.coinswap_parameters.tx3_amounts["script"], dest_addr) self.tx3redeem.sign_at_index(self.keyset["key_TX3_lock"][0], 0) wallet_name = cs_single().bc_interface.get_wallet_name(self.wallet) msg, success = self.tx3redeem.push() cslog.info("Redeem tx: ") cslog.info(self.tx3redeem) if not success: cslog.info("RPC error message: " + msg) cslog.info("Failed to broadcast TX3 redeem; here is raw form: ") cslog.info(self.tx3redeem.fully_signed_tx) cslog.info("Readable form: ") cslog.info(self.tx3redeem) return False return True
def redeem_tx2_with_secret(self): #Broadcast TX3 msg, success = self.tx2.push() if not success: cslog.info("RPC error message: " + msg) cslog.info("Failed to broadcast TX2; here is raw form: ") cslog.info(self.tx2.fully_signed_tx) return False #**CONSTRUCT TX2-redeem-secret; use a fresh address to redeem dest_addr = self.wallet.get_new_addr(0, 1, True) tx2redeem_secret = CoinSwapRedeemTX23Secret(self.secret, self.coinswap_parameters.pubkeys["key_TX2_secret"], self.coinswap_parameters.timeouts["LOCK0"], self.coinswap_parameters.pubkeys["key_TX2_lock"], self.tx2.txid+":0", self.coinswap_parameters.tx2_amounts["script"], dest_addr) tx2redeem_secret.sign_at_index(self.keyset["key_TX2_secret"][0], 0) wallet_name = cs_single().bc_interface.get_wallet_name(self.wallet) msg, success = tx2redeem_secret.push() cslog.info("Redeem tx: ") cslog.info(tx2redeem_secret) if not success: cslog.info("RPC error message: " + msg) cslog.info("Failed to broadcast TX2 redeem; here is raw form: ") cslog.info(tx2redeem_secret.fully_signed_tx) cslog.info(tx2redeem_secret) return False else: cslog.info("Successfully redeemed funds via TX2, to address: "+\ dest_addr + ", in txid: " +\ tx2redeem_secret.txid) return True
def broadcast_tx0(self, acceptedmsg): #We have completed first-phase processing. #We push our TX0 and wait for the other side to complete by #pushing TX1. accepted, msg = acceptedmsg if not accepted: return (False, "Counterparty did not accept our TX3 signature, " + \ "error message: " + msg) errmsg, success = self.tx0.push() if not success: return (False, "Failed to push TX0, errmsg: " + errmsg) #Monitor the output address of TX0 by importing cs_single().bc_interface.rpc( "importaddress", [self.tx0.output_address, "joinmarket-notify", False]) return (True, "Pushed TX0 OK: " + self.tx0.txid)
def push_tx1(self): """Having seen TX0 confirmed, broadcast TX1 and wait for confirmation. """ errmsg, success = self.tx1.push() if not success: return (False, "Failed to push TX1") #Monitor the output address of TX1 by importing cs_single().bc_interface.rpc( "importaddress", [self.tx1.output_address, "joinmarket-notify", False]) #Wait until TX1 seen before confirming phase2 ready. self.loop = task.LoopingCall( self.check_for_phase1_utxos, [self.tx1.txid + ":" + str(self.tx1.pay_out_index)], self.receive_confirmation_tx_0_1) self.loop.start(3.0) return (True, "TX1 broadcast OK")
def negotiate_coinswap_parameters(self, params): #receive parameters and ephemeral keys, destination address from Alice. #Send back ephemeral keys and destination address, or rejection, #if invalid, to Alice. for k in self.required_key_names: self.coinswap_parameters.set_pubkey(k, self.keyset[k][1]) try: self.coinswap_parameters.set_tx01_amounts(params[0]) self.coinswap_parameters.set_tx24_recipient_amounts(params[1]) self.coinswap_parameters.set_tx35_recipient_amounts(params[2]) self.coinswap_parameters.set_pubkey("key_2_2_AC_0", params[3]) self.coinswap_parameters.set_pubkey("key_2_2_CB_1", params[4]) self.coinswap_parameters.set_pubkey("key_TX2_lock", params[5]) self.coinswap_parameters.set_pubkey("key_TX3_secret", params[6]) #Client's locktimes must be in an acceptable range. #Note that the tolerances here are hardcoded, probably a TODO. #(Although it'll be less complicated if everybody runs with one #default for the locktimes). cbh = get_current_blockheight() my_lock0 = cs_single().config.getint("TIMEOUT", "lock_client") my_lock1 = cs_single().config.getint("TIMEOUT", "lock_server") if params[7] not in range(cbh + my_lock0 - 10, cbh + my_lock0 + 11): return (False, "Counterparty LOCK0 out of range") if params[8] not in range(cbh + my_lock1 - 10, cbh + my_lock1 + 11): return (False, "Counterparty LOCK1 out of range") self.coinswap_parameters.set_timeouts(params[7], params[8]) self.coinswap_parameters.set_tx5_address(params[9]) except: return (False, "Invalid parameter set from counterparty, abandoning") #on receipt of valid response, complete the CoinswapPublicParameters instance for k in self.required_key_names: self.coinswap_parameters.set_pubkey(k, self.keyset[k][1]) if not self.coinswap_parameters.is_complete(): cslog.debug("addresses: " + str(self.coinswap_parameters.addresses_complete)) cslog.debug("pubkeys: " + str(self.coinswap_parameters.pubkeys_complete)) cslog.debug("timeouts: " + str(self.coinswap_parameters.timeouts_complete)) return (False, "Coinswap parameters is not complete") #first entry confirms acceptance of parameters to_send = [True, self.coinswap_parameters.pubkeys["key_2_2_AC_1"], self.coinswap_parameters.pubkeys["key_2_2_CB_0"], self.coinswap_parameters.pubkeys["key_TX2_secret"], self.coinswap_parameters.pubkeys["key_TX3_lock"], self.coinswap_parameters.tx4_address] return (to_send, "OK")
def make_wallets(n, wallet_structures=None, mean_amt=1, sdev_amt=0, start_index=0, fixed_seeds=None, test_wallet=False, passwords=None): '''n: number of wallets to be created wallet_structure: array of n arrays , each subarray specifying the number of addresses to be populated with coins at each depth (for now, this will only populate coins into 'receive' addresses) mean_amt: the number of coins (in btc units) in each address as above sdev_amt: if randomness in amouts is desired, specify here. Returns: a dict of dicts of form {0:{'seed':seed,'wallet':Wallet object},1:..,} Default Wallet constructor is joinmarket.Wallet, else use TestWallet, which takes a password parameter as in the list passwords. ''' if len(wallet_structures) != n: raise Exception("Number of wallets doesn't match wallet structures") if not fixed_seeds: seeds = chunks(binascii.hexlify(os.urandom(15 * n)), 15 * 2) else: seeds = fixed_seeds wallets = {} for i in range(n): if test_wallet: w = TestWallet(seeds[i], None, max_mix_depth=3, pwd=passwords[i]) else: w = Wallet(seeds[i], None, max_mix_depth=3) wallets[i + start_index] = {'seed': seeds[i], 'wallet': w} for j in range(3): for k in range(wallet_structures[i][j]): deviation = sdev_amt * random.random() amt = mean_amt - sdev_amt / 2.0 + deviation if amt < 0: amt = 0.001 amt = float(Decimal(amt).quantize(Decimal(10)**-8)) cs_single().bc_interface.grab_coins( wallets[i + start_index]['wallet'].get_external_addr(j), amt) #reset the index so the coins can be seen if running in same script wallets[ i + start_index]['wallet'].index[j][0] -= wallet_structures[i][j] return wallets
def set_handshake_parameters(self): """Sets the conditions under which Carol is prepared to do a coinswap. """ c = cs_single().config self.source_chain = c.get("SERVER", "source_chain") self.destination_chain = c.get("SERVER", "destination_chain") self.minimum_amount = c.getint("SERVER", "minimum_amount") self.maximum_amount = c.getint("SERVER", "maximum_amount")
def get_ssl_context(): """Construct an SSL context factory from the user's privatekey/cert. TODO: document set up for server operators. """ pkcdata = {} for x, y in zip(["ssl_private_key_location", "ssl_certificate_location"], ["key.pem", "cert.pem"]): if cs_single().config.get("SERVER", x) == "0": sslpath = os.path.join(cs_single().homedir, "ssl") if not os.path.exists(sslpath): print("No ssl configuration in home directory, please read " "installation instructions and try again.") sys.exit(0) pkcdata[x] = os.path.join(sslpath, y) else: pkcdata[x] = cs_single().config.get("SERVER", x) return ssl.DefaultOpenSSLContextFactory(pkcdata["ssl_private_key_location"], pkcdata["ssl_certificate_location"])
def get_state_machine_callbacks(self): return [(self.handshake, False, -1), (self.negotiate_coinswap_parameters, False, -1), (self.receive_tx0_hash_tx2sig, False, -1), (self.send_tx1id_tx2_sig_tx3_sig, True, -1), (self.receive_tx3_sig, False, -1), #alice waits for confirms before sending secret; this accounts #for propagation delays. (self.push_tx1, False, cs_single().one_confirm_timeout * cs_single().config.getint( "TIMEOUT", "tx01_confirm_wait")), #we also wait for the confirms our side (self.receive_secret, False, cs_single().one_confirm_timeout * cs_single().config.getint( "TIMEOUT", "tx01_confirm_wait")), #alice waits for confirms on TX5 before sending TX4 sig (self.send_tx5_sig, True, -1), (self.receive_tx4_sig, False, cs_single().one_confirm_timeout), (self.broadcast_tx4, True, -1)]
def wait_for_tx5_confirmation(self, confs=1): """Looping task to wait for TX5 on network before TX4. """ result = cs_single().bc_interface.query_utxo_set( [self.tx5.txid + ":0"], includeconf=True) if None in result: return for u in result: if u['confirms'] < confs: return self.loop_tx5.stop() self.sm.tick()
def wait_for_tx4_confirmed(self): result = cs_single().bc_interface.query_utxo_set([self.tx4.txid+":0"], includeconf=True) if None in result: return for u in result: if u['confirms'] < 1: return self.tx4_loop.stop() self.tx4_confirmed = True cslog.info("Carol received: " + self.tx4.txid + ", now ending.") self.quit()
def handshake(self): """Record the state of the wallet at the start of the process. Send a handshake message to Carol with required parameters for this Coinswap. """ self.bbmb = self.wallet.get_balance_by_mixdepth(verbose=False) to_send = { "coinswapcs_version": cs_single().CSCS_VERSION, "session_id": self.coinswap_parameters.session_id, "tx01_confirm_wait": cs_single().config.getint("TIMEOUT", "tx01_confirm_wait"), "source_chain": "BTC", "destination_chain": "BTC", "amount": self.coinswap_parameters.tx0_amount } self.send(to_send) return (True, "Handshake OK")
def add_watchonly_addresses(self, addr_list, wallet_name): cslog.debug('importing ' + str(len(addr_list)) + ' addresses into account ' + wallet_name) for addr in addr_list: self.rpc('importaddress', [addr, wallet_name, False]) if cs_single().config.get( "BLOCKCHAIN", "blockchain_source") != 'regtest': #pragma: no cover #Exit conditions cannot be included in tests print('restart Bitcoin Core with -rescan if you\'re ' 'recovering an existing wallet from backup seed') print(' otherwise just restart this joinmarket script') sys.exit(0)
def sync_wallet(wallet, fast=False): """Wrapper function to choose fast syncing where it's both possible and requested. """ if fast and (isinstance(cs_single().bc_interface, BitcoinCoreInterface) or isinstance(cs_single().bc_interface, RegtestBitcoinCoreInterface)): cs_single().bc_interface.sync_wallet(wallet, fast=True) else: cs_single().bc_interface.sync_wallet(wallet)
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 = ins_full.keys() #random output address and change addr output_addr = wallet.get_new_addr(1, 1, True) if not output_addr else output_addr change_addr = wallet.get_new_addr(1, 0, True) 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 }] tx = btc.mktx(ins, outs) de_tx = btc.deserialize(tx) for index, ins in enumerate(de_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) addr = ins_full[utxo]['address'] priv = wallet.get_key_from_addr(addr) if index % 2: priv = binascii.unhexlify(priv) tx = btc.sign(tx, index, priv, hashcode=hashcode) #pushtx returns False on any error print btc.deserialize(tx) push_succeed = cs_single().bc_interface.pushtx(tx) if push_succeed: return btc.txhash(tx) else: return False
def run(self): notify_host = 'localhost' notify_port = 62602 # defaults config = cs_single().config if 'notify_host' in config.options("BLOCKCHAIN"): notify_host = config.get("BLOCKCHAIN", "notify_host").strip() if 'notify_port' in config.options("BLOCKCHAIN"): notify_port = int(config.get("BLOCKCHAIN", "notify_port")) for inc in range(10): hostport = (notify_host, notify_port + inc) try: httpd = BaseHTTPServer.HTTPServer(hostport, NotifyRequestHeader) except Exception: continue httpd.btcinterface = self.btcinterface cslog.debug('started bitcoin core notify listening thread, host=' + str(notify_host) + ' port=' + str(hostport[1])) httpd.serve_forever() cslog.debug('failed to bind for bitcoin core notify listening')
def scan_blockchain_for_secret(self): """Only required by Carol; in cases where the wallet monitoring fails (principally because a secret-redeeming transaction by Alice occurred when we were not on-line), we must be able to find the secret directly from scanning the blockchain. This could be achieved with indexing on our Bitcoin Core instance, but since this requires a lot of resources, it's simpler to directly parse the relevant blocks. """ bh = get_current_blockheight() starting_blockheight = self.coinswap_parameters.timeouts[ "LOCK0"] - cs_single().config.getint("TIMEOUT", "lock_client") while bh >= starting_blockheight: found_txs = get_transactions_from_block(bh) for t in found_txs: retval = get_secret_from_vin(t['ins'], self.hashed_secret) if retval: self.secret = retval return True bh -= 1 cslog.info("Failed to find secret from scanning blockchain.") return False
def main_server(options, wallet, test_data=None): """The use_ssl option is only for tests, and flags that case. """ if test_data and not test_data['use_ssl']: cs_single().config.set("SERVER", "use_ssl", "false") cs_single().bc_interface.start_unspent_monitoring(wallet) #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(cs_single().bc_interface, RegtestBitcoinCoreInterface): cs_single().bc_interface.tick_forward_chain_interval = 2 cs_single().bc_interface.simulating = True cs_single().config.set("BLOCKCHAIN", "notify_port", "62652") cs_single().config.set("BLOCKCHAIN", "rpc_host", "127.0.0.2") #if restart option selected, read state and backout #(TODO is to attempt restarting normally before backing out) if options.recover: session_id = options.recover carol = CoinSwapCarol(wallet, 'carolstate') carol.bbmb = wallet.get_balance_by_mixdepth(verbose=False) carol.load(sessionid=session_id) carol.backout("Recovering from shutdown") reactor.run() return #TODO currently ignores server setting here and uses localhost port = cs_single().config.getint("SERVER", "port") testing_mode = True if test_data else False carol_class = test_data['alt_c_class'] if test_data and \ test_data['alt_c_class'] else CoinSwapCarol fcs = test_data["fail_carol_state"] if test_data else None #Hidden service has first priority if cs_single().config.get("SERVER", "use_onion") != "false": s = server.Site( CoinSwapCarolJSONServer(wallet, testing_mode=testing_mode, carol_class=carol_class, fail_carol_state=fcs)) hiddenservice_dir = os.path.join(cs_single().homedir, "hiddenservice") if not os.path.exists(hiddenservice_dir): os.makedirs(hiddenservice_dir) if 'hs_dir' in cs_single().config.options('SERVER'): hiddenservice_dir = cs_single().config.get("SERVER", "hs_dir") d = start_tor(s, cs_single().config.getint("SERVER", "onion_port"), hiddenservice_dir) #Any callbacks after Tor is inited can be added here with d.addCallback elif cs_single().config.get("SERVER", "use_ssl") != "false": reactor.listenSSL(int(port), server.Site( CoinSwapCarolJSONServer( wallet, testing_mode=testing_mode, carol_class=carol_class, fail_carol_state=fcs)), contextFactory=get_ssl_context()) else: cslog.info("WARNING! Serving over HTTP, no TLS used!") reactor.listenTCP( int(port), server.Site( CoinSwapCarolJSONServer(wallet, testing_mode=testing_mode, carol_class=carol_class, fail_carol_state=fcs))) if not test_data: reactor.run()
def negotiate_coinswap_parameters(self, params): #receive parameters and ephemeral keys, destination address from Alice. #Send back ephemeral keys and destination address, or rejection, #if invalid, to Alice. for k in self.required_key_names: self.coinswap_parameters.set_pubkey(k, self.keyset[k][1]) try: self.coinswap_parameters.set_pubkey("key_2_2_AC_0", params[0]) self.coinswap_parameters.set_pubkey("key_2_2_CB_1", params[1]) self.coinswap_parameters.set_pubkey("key_TX2_lock", params[2]) self.coinswap_parameters.set_pubkey("key_TX3_secret", params[3]) #Client's locktimes must be in the acceptable range. cbh = get_current_blockheight() serverlockrange = cs_single().config.get("SERVER", "server_locktime_range") serverlockmin, serverlockmax = [ int(x) for x in serverlockrange.split(",")] clientlockrange = cs_single().config.get("SERVER", "client_locktime_range") clientlockmin, clientlockmax = [ int(x) for x in clientlockrange.split(",")] if params[4] not in range(cbh + clientlockmin, cbh + clientlockmax+1): return (False, "Counterparty LOCK0 out of range") if params[5] not in range(cbh + serverlockmin, cbh + serverlockmax+1): return (False, "Counterparty LOCK1 out of range") #This is enforced in CoinSwapPublicParameters with assert, it must #not trigger in the server from external input. if params[4] <= params[5]: return (False, "LOCK1 must be before LOCK0") self.coinswap_parameters.set_timeouts(params[4], params[5]) self.coinswap_parameters.set_addr_data(addr5=params[6]) except Exception as e: return (False, "Invalid parameter set from counterparty, abandoning: " + \ repr(e)) #on receipt of valid response, complete the CoinswapPublicParameters instance for k in self.required_key_names: self.coinswap_parameters.set_pubkey(k, self.keyset[k][1]) if not self.coinswap_parameters.is_complete(): cslog.debug("addresses: " + str(self.coinswap_parameters.addresses_complete)) cslog.debug("pubkeys: " + str(self.coinswap_parameters.pubkeys_complete)) cslog.debug("timeouts: " + str(self.coinswap_parameters.timeouts_complete)) return (False, "Coinswap parameters is not complete") #Calculate the fee required for the swap now we have valid data. #The coinswap fee is assessed against the base amount proposed by the client. self.coinswap_parameters.set_coinswap_fee( self.coinswap_parameters.fee_policy.get_fee( self.coinswap_parameters.base_amount)) #Calculate a one time blinding amount for this coinswap within the #configured max and min bl_amt = random.randint(cs_single().config.getint("SERVER", "blinding_amount_min"), cs_single().config.getint("SERVER", "blinding_amount_max")) #TODO check that we can serve an amount up to base_amt + bl_amt + csfee; #otherwise need to retry selecting a blinding factor (or do something more #intelligent). self.coinswap_parameters.set_blinding_amount(bl_amt) #first entry confirms acceptance of parameters to_send = [True, self.coinswap_parameters.pubkeys["key_2_2_AC_1"], self.coinswap_parameters.pubkeys["key_2_2_CB_0"], self.coinswap_parameters.pubkeys["key_TX2_secret"], self.coinswap_parameters.pubkeys["key_TX3_lock"], self.coinswap_parameters.output_addresses["tx4_address"], self.coinswap_parameters.coinswap_fee, self.coinswap_parameters.blinding_amount, self.coinswap_parameters.output_addresses["tx2_carol_address"], self.coinswap_parameters.output_addresses["tx3_carol_address"], self.coinswap_parameters.output_addresses["tx5_carol_address"], self.coinswap_parameters.session_id] #We can now initiate file logging also; .log will be automatically appended cs_single().logs_path = os.path.join(cs_single().homedir, "logs", self.state_file) return (to_send, "OK")
def main_cs(test_data=None): #twisted logging (TODO disable for non-debug runs) if test_data: wallet_name, args, options, use_ssl, alt_class, alt_c_class, fail_alice_state, fail_carol_state = test_data server, port, usessl = parse_server_string(options.serverport) else: parser = get_coinswap_parser() (options, args) = parser.parse_args() #Will only be used by client server, port, usessl = parse_server_string(options.serverport) if options.checkonly: #no need for any more data; just query alice_client = CoinSwapJSONRPCClient(server[2:], port, usessl=usessl) reactor.callWhenRunning(alice_client.send_poll_unsigned, "status", print_status) reactor.run() return log.startLogging(sys.stdout) load_coinswap_config() wallet_name = args[0] #depth 0: spend in, depth 1: receive out, depth 2: for backout transactions. max_mix_depth = 3 wallet_dir = os.path.join(cs_single().homedir, 'wallets') if not os.path.exists(os.path.join(wallet_dir, wallet_name)): wallet = SegwitWallet(wallet_name, None, max_mix_depth, 6, wallet_dir=wallet_dir) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = SegwitWallet(wallet_name, pwd, max_mix_depth, 6, wallet_dir=wallet_dir) except WalletError: print("Wrong password, try again.") continue except Exception as e: print("Failed to load wallet, error message: " + repr(e)) sys.exit(0) break #for testing main script (not test framework), need funds. if not test_data and isinstance(cs_single().bc_interface, RegtestBitcoinCoreInterface): for i in range(3): cs_single().bc_interface.grab_coins( wallet.get_new_addr(0, 0, True), 2.0) wallet.index[0][0] -= 3 time.sleep(3) sync_wallet(wallet, fast=options.fastsync) if test_data: cs_single().bc_interface.wallet_synced = True wallet.used_coins = None if options.serve: #sanity check that client params were not provided: if len(args) > 1: print("Extra parameters provided for running as server. " "Are you sure you didn't want to run as client?") sys.exit(0) if not test_data: main_server(options, wallet) else: main_server( options, wallet, { 'use_ssl': use_ssl, 'alt_c_class': alt_c_class, 'fail_carol_state': fail_carol_state }) return wallet.get_balance_by_mixdepth() return if not options.recover: target_amount = int(args[1]) #Reset the targetting for backout transactions #TODO must be removed/changed for updated fees handling oldtarget = cs_single().config.get("POLICY", "tx_fees") newtarget = cs_single().config.getint("POLICY", "backout_fee_target") multiplier = float(cs_single().config.get("POLICY", "backout_fee_multiplier")) cs_single().config.set("POLICY", "tx_fees", str(newtarget)) tx23fee = estimate_tx_fee((1, 2, 2), 1, txtype='p2shMofN') tx23fee = int(multiplier * tx23fee) tx24_recipient_amount = target_amount - tx23fee tx35_recipient_amount = target_amount - tx23fee cs_single().config.set("POLICY", "tx_fees", oldtarget) #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(cs_single().bc_interface, RegtestBitcoinCoreInterface): cs_single().bc_interface.tick_forward_chain_interval = 2 cs_single().bc_interface.simulating = True cs_single().config.set("BLOCKCHAIN", "notify_port", "62652") cs_single().config.set("BLOCKCHAIN", "rpc_host", "127.0.0.2") #if restart option selected, read state and backout if options.recover: session_id = options.recover alice = CoinSwapAlice(wallet, 'alicestate') alice.bbmb = wallet.get_balance_by_mixdepth(verbose=False) alice.load(sessionid=session_id) alice.backout("Recovering from shutdown") reactor.run() return if len(args) > 2: tx5address = args[2] if not validate_address(tx5address): print("Invalid address: ", tx5address) sys.exit(0) else: #Our destination address should be in a separate mixdepth tx5address = wallet.get_new_addr(1, 1, True) #instantiate the parameters, but don't yet have the ephemeral pubkeys #or destination addresses. #TODO figure out best estimate incl. priority btcfee_est = estimate_tx_fee((1, 2, 2), 1, txtype='p2shMofN') cpp = CoinSwapPublicParameters(base_amount=target_amount, bitcoin_fee=btcfee_est) cpp.set_addr_data(addr5=tx5address) testing_mode = True if test_data else False aliceclass = alt_class if test_data and alt_class else CoinSwapAlice if test_data and fail_alice_state: alice = aliceclass(wallet, 'alicestate', cpp, testing_mode=testing_mode, fail_state=fail_alice_state) else: if testing_mode or options.checkfee: alice = aliceclass(wallet, 'alicestate', cpp, testing_mode=testing_mode) else: alice = aliceclass(wallet, 'alicestate', cpp, testing_mode=testing_mode, fee_checker="cli") alice_client = CoinSwapJSONRPCClient(server[2:], port, alice.sm.tick, alice.backout, usessl) alice.set_jsonrpc_client(alice_client) reactor.callWhenRunning(alice_client.send_poll_unsigned, "status", alice.check_server_status) if not test_data: reactor.run() if test_data: return alice
def check_server_status(self, status): """Retrieve the server status and validate that it allows coinswaps with the chosen parameters to start. """ c = cs_single().config assert self.sm.state == 0 if not all([ x in status.keys() for x in [ "source_chain", "destination_chain", "cscs_version", "minimum_amount", "maximum_amount", "busy", "testnet", "tx01_confirm_wait" ] ]): cslog.info("Server gave invalid status response.") reactor.stop() elif status["source_chain"] != "BTC" or status[ "destination_chain"] != "BTC": cslog.info("Server is not BTC-BTC chain") reactor.stop() elif status["cscs_version"] != cs_single().CSCS_VERSION: cslog.info("Server has wrong CSCS version, aborting") reactor.stop() elif status["busy"] or status["maximum_amount"] < 0: cslog.info("Server is not currently available") reactor.stop() elif status["testnet"] == True and c.get("BLOCKCHAIN", "network") != "testnet": cslog.info("Server is not for correct network (testnet/mainnet)") reactor.stop() elif self.coinswap_parameters.base_amount < status["minimum_amount"]: cslog.info("Amount too small for server") reactor.stop() elif self.coinswap_parameters.base_amount > status["maximum_amount"]: cslog.info("Amount too large for server") reactor.stop() elif c.getint( "TIMEOUT", "lock_client") > status["locktimes"]["lock_client"]["max"]: cslog.info("Our client locktime (lock 0) is too high.") reactor.stop() elif c.getint( "TIMEOUT", "lock_client") < status["locktimes"]["lock_client"]["min"]: cslog.info("Our client locktime (lock 0) is too small.") reactor.stop() elif c.getint( "TIMEOUT", "lock_server") > status["locktimes"]["lock_server"]["max"]: cslog.info("Our server locktime (lock 1) is too high.") reactor.stop() elif c.getint( "TIMEOUT", "lock_server") < status["locktimes"]["lock_server"]["min"]: cslog.info("Our server locktime (lock 1) is too small.") reactor.stop() elif c.getint("TIMEOUT", "tx01_confirm_wait") not in range( status["tx01_confirm_wait"]["min"], status["tx01_confirm_wait"]["max"] + 1): cslog.info("Our tx01 confirm wait is not accepted by the server.") reactor.stop() else: cslog.info("Server settings are compatible, continuing...") self.sm.tick()
def complete_negotiation(self, carol_response): """Receive Carol's coinswap parameters. """ cslog.debug('Carol response for param negotiation: ' + str(carol_response)) if not carol_response[0]: return (False, "Negative response from Carol in negotiation") #on receipt of valid response, complete the CoinswapPublicParameters instance #note that we only finish our ephemeral pubkeys part here, after they're #accepted for k in self.required_key_names: self.coinswap_parameters.set_pubkey(k, self.keyset[k][1]) self.coinswap_parameters.set_pubkey("key_2_2_AC_1", carol_response[0][1]) self.coinswap_parameters.set_pubkey("key_2_2_CB_0", carol_response[0][2]) self.coinswap_parameters.set_pubkey("key_TX2_secret", carol_response[0][3]) self.coinswap_parameters.set_pubkey("key_TX3_lock", carol_response[0][4]) #on acceptance, fix the tx01confirmwait in the CSPP instance; since we only #do one CS at a time, could stick with the global config, but better to be #consistent with the logic in the server (for code sharing). self.coinswap_parameters.set_tx01_confirm_wait( cs_single().config.getint("TIMEOUT", "tx01_confirm_wait")) proposed_fee = carol_response[0][6] if self.fee_checker: if not self.fee_checker(proposed_fee): return (False, "Server's proposed fee: " + str(proposed_fee) + \ " is not accepted.") else: cslog.info("Server proposed fee: " + str(proposed_fee) + \ ", accepted.") self.coinswap_parameters.set_coinswap_fee(carol_response[0][6]) proposed_blinding_amount = carol_response[0][7] #Is this blinding factor good enough according to our wishes? if proposed_blinding_amount < cs_single().config.getint( "POLICY", "minimum_blinding_amount"): return (False, "Blinding amount is too small for us: " + \ str(proposed_blinding_amount) + "but we require at least: " + \ str(cs_single().config.getint("POLICY", "minimum_blinding_amount"))) self.coinswap_parameters.set_blinding_amount(proposed_blinding_amount) self.coinswap_parameters.set_addr_data( addr4=carol_response[0][5], addr_2_carol=carol_response[0][8], addr_3_carol=carol_response[0][9], addr_5_carol=carol_response[0][10]) proposed_sessionid = carol_response[0][11] if not len(proposed_sessionid) == 32: return (False, "Invalid sessionid proposal: " + str(proposed_sessionid)) self.coinswap_parameters.set_session_id(proposed_sessionid) #The state file name setting had to be deferred until here: self.state_file = self.state_file + proposed_sessionid + ".json" #We can now initiate file logging also; .log will be automatically appended cs_single().logs_path = os.path.join(cs_single().homedir, "logs", self.state_file) if not self.coinswap_parameters.is_complete(): return ( False, "Coinswap public parameter negotiation failed, incomplete.") return (True, "Coinswap public parameter negotiation OK")