예제 #1
0
 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")
예제 #2
0
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
예제 #3
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)
     ]
예제 #4
0
    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')
예제 #5
0
 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")
예제 #6
0
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)
예제 #7
0
 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
예제 #8
0
    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()
예제 #9
0
 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
예제 #10
0
 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
예제 #11
0
 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)
예제 #12
0
 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")
예제 #13
0
    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")
예제 #14
0
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
예제 #15
0
 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")
예제 #16
0
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"])
예제 #17
0
 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)]
예제 #18
0
 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()
예제 #19
0
 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()
예제 #20
0
 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")
예제 #21
0
 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)
예제 #22
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)
예제 #23
0
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
예제 #24
0
 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')
예제 #25
0
 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
예제 #26
0
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()
예제 #27
0
    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")
예제 #28
0
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
예제 #29
0
 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()
예제 #30
0
 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")