From e7afd5e67d95b70692e5b14f54645a17a7743d52 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Fri, 4 Dec 2020 19:06:50 +0200 Subject: [PATCH] Display warning when wallet seedid doesn't match expected. --- README.md | 52 +--------- basicswap/basicswap.py | 145 +++++++++++++++++++--------- basicswap/chainparams.py | 9 ++ basicswap/http_server.py | 1 + basicswap/interface_btc.py | 8 ++ basicswap/interface_part.py | 4 + basicswap/interface_xmr.py | 6 ++ basicswap/templates/wallets.html | 3 +- doc/notes.md | 53 ++++++++++ tests/basicswap/test_wallet_init.py | 18 +++- 10 files changed, 199 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 6fcb96f..31fa48a 100644 --- a/README.md +++ b/README.md @@ -19,54 +19,4 @@ In the future it should be possible to use data from explorers instead of runnin Not ready for real-world use. -Features still required (of many): - - Cached addresses must be regenerated after use. - - Option to lookup data from public explorers / nodes. - - Ability to swap coin-types without running nodes for all coin-types - - More swap protocols - - Method to load mnemonic into Particl. - - Load seeds for other wallets from same mnemonic. - - COIN must be defined per coin. - - -## Seller first protocol: - -Seller sends the 1st transaction. - -1. Seller posts offer. - - smsg from seller to network - coin-from - coin-to - amount-from - rate - min-amount - time-valid - -2. Buyer posts bid: - - smsg from buyer to seller - offerid - amount - proof-of-funds - address_to_buyer - time-valid - -3. Seller accepts bid: - - verifies proof-of-funds - - generates secret - - submits initiate tx to coin-from network - - smsg from seller to buyer - txid - initiatescript (includes pkhash_to_seller as the pkhash_refund) - -4. Buyer participates: - - inspects initiate tx in coin-from network - - submits participate tx in coin-to network - -5. Seller redeems: - - constructs participatescript - - inspects participate tx in coin-to network - - redeems from participate tx revealing secret - -6. Buyer redeems: - - scans coin-to network for seller-redeem tx - - redeems from initiate tx with revealed secret +Discuss development and help with testing in the matrix channel [#basicswap:matrix.org](https://riot.im/app/#/room/#basicswap:matrix.org) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index dcaa737..308f623 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -621,19 +621,39 @@ class BasicSwap(BaseApp): if self.coin_clients[c]['connection_type'] == 'rpc': self.waitForDaemonRPC(c) - core_version = self.coin_clients[c]['interface'].getDaemonVersion() - self.log.info('%s Core version %d', chainparams[c]['name'].capitalize(), core_version) + ci = self.ci(c) + core_version = ci.getDaemonVersion() + self.log.info('%s Core version %d', ci.coin_name(), core_version) self.coin_clients[c]['core_version'] = core_version - if c == Coins.XMR: - self.coin_clients[c]['interface'].ensureWalletExists() - if c == Coins.PART: - self.coin_clients[c]['have_spent_index'] = self.coin_clients[c]['interface'].haveSpentIndex() + self.coin_clients[c]['have_spent_index'] = ci.haveSpentIndex() # Sanity checks if self.callcoinrpc(c, 'getstakinginfo')['enabled'] is not False: - self.log.warning('%s staking is not disabled.', chainparams[c]['name'].capitalize()) + self.log.warning('%s staking is not disabled.', ci.coin_name()) + elif c == Coins.XMR: + ci.ensureWalletExists() + + expect_address = self.getStringKV('main_wallet_addr_' + chainparams[c]['name']) + self.log.debug('[rm] expect_address %s', expect_address) + if expect_address is None: + self.log.warning('Can\'t find expected main wallet address for coin {}'.format(ci.coin_name())) + else: + if expect_address == ci.getMainWalletAddress(): + ci.setWalletSeedWarning(False) + else: + self.log.warning('Wallet for coin {} not derived from swap seed.'.format(ci.coin_name())) + else: + expect_seedid = self.getStringKV('main_wallet_seedid_' + chainparams[c]['name']) + self.log.debug('[rm] expect_seedid %s', expect_seedid) + if expect_seedid is None: + self.log.warning('Can\'t find expected wallet seed id for coin {}'.format(ci.coin_name())) + else: + if expect_seedid == ci.getWalletSeedID(): + ci.setWalletSeedWarning(False) + else: + self.log.warning('Wallet for coin {} not derived from swap seed.'.format(ci.coin_name())) self.initialise() @@ -702,6 +722,8 @@ class BasicSwap(BaseApp): raise ValueError('{} chain is still syncing, currently at {}.'.format(self.coin_clients[c]['name'], synced)) def initialiseWallet(self, coin_type): + if coin_type == Coins.PART: + return ci = self.ci(coin_type) self.log.info('Initialising {} wallet.'.format(ci.coin_name())) @@ -709,20 +731,63 @@ class BasicSwap(BaseApp): key_view = self.getWalletKey(coin_type, 1, for_ed25519=True) key_spend = self.getWalletKey(coin_type, 2, for_ed25519=True) ci.initialiseWallet(key_view, key_spend) + root_address = ci.getAddressFromKeys(key_view, key_spend) + + key_str = 'main_wallet_addr_' + chainparams[coin_type]['name'] + self.setStringKV(key_str, root_address) return root_key = self.getWalletKey(coin_type, 1) + root_hash = ci.getAddressHashFromKey(root_key) ci.initialiseWallet(root_key) + key_str = 'main_wallet_seedid_' + chainparams[coin_type]['name'] + self.setStringKV(key_str, root_hash.hex()) + def setIntKV(self, str_key, int_val): - session = scoped_session(self.session_factory) - kv = session.query(DBKVInt).filter_by(key=str_key).first() - if not kv: - kv = DBKVInt(key=str_key, value=int_val) - session.add(kv) - session.commit() - session.close() - session.remove() + self.mxDB.acquire() + try: + session = scoped_session(self.session_factory) + kv = session.query(DBKVInt).filter_by(key=str_key).first() + if not kv: + kv = DBKVInt(key=str_key, value=int_val) + else: + kv.value = int_val + session.add(kv) + session.commit() + session.close() + session.remove() + finally: + self.mxDB.release() + + def setStringKV(self, str_key, str_val): + self.mxDB.acquire() + try: + session = scoped_session(self.session_factory) + kv = session.query(DBKVString).filter_by(key=str_key).first() + if not kv: + kv = DBKVString(key=str_key, value=str_val) + else: + kv.value = str_val + session.add(kv) + session.commit() + session.close() + session.remove() + finally: + self.mxDB.release() + + def getStringKV(self, str_key): + self.mxDB.acquire() + try: + session = scoped_session(self.session_factory) + v = session.query(DBKVString).filter_by(key=str_key).first() + if not v: + return None + return v.value + finally: + session.close() + session.remove() + self.mxDB.release() def activateBid(self, session, bid): if bid.bid_id in self.swaps_in_progress: @@ -756,7 +821,7 @@ class BasicSwap(BaseApp): # TODO process addresspool if bid has previously been abandoned - def deactivateBid(self, offer, bid): + def deactivateBid(self, session, offer, bid): # Remove from in progress self.log.debug('Removing bid from in-progress: %s', bid.bid_id.hex()) self.swaps_in_progress.pop(bid.bid_id, None) @@ -1126,25 +1191,8 @@ class BasicSwap(BaseApp): def cacheNewAddressForCoin(self, coin_type): self.log.debug('cacheNewAddressForCoin %s', coin_type) key_str = 'receive_addr_' + chainparams[coin_type]['name'] - session = scoped_session(self.session_factory) addr = self.getReceiveAddressForCoin(coin_type) - self.mxDB.acquire() - try: - session = scoped_session(self.session_factory) - try: - kv = session.query(DBKVString).filter_by(key=key_str).first() - kv.value = addr - except Exception: - kv = DBKVString( - key=key_str, - value=addr - ) - session.add(kv) - session.commit() - session.close() - session.remove() - finally: - self.mxDB.release() + self.setStringKV(key_str, addr) return addr def getCachedAddressForCoin(self, coin_type): @@ -1838,9 +1886,9 @@ class BasicSwap(BaseApp): # Mark bid as abandoned, no further processing will be done bid.setState(BidStates.BID_ABANDONED) + self.deactivateBid(session, offer, bid) + session.add(bid) session.commit() - - self.deactivateBid(offer, bid) finally: session.close() session.remove() @@ -3986,6 +4034,7 @@ class BasicSwap(BaseApp): self.setBidError(bid_id, bid, str(ex)) # Update copy of bid in swaps_in_progress + assert(bid_id in self.swaps_in_progress) self.swaps_in_progress[bid_id] = (bid, offer) def processXmrSplitMessage(self, msg): @@ -4112,7 +4161,7 @@ class BasicSwap(BaseApp): self.setBidError(bid_id, v[0], str(ex)) for bid_id, bid, offer in to_remove: - self.deactivateBid(offer, bid) + self.deactivateBid(None, offer, bid) self._last_checked_progress = now if now - self._last_checked_watched >= self.check_watched_seconds: @@ -4157,12 +4206,20 @@ class BasicSwap(BaseApp): if has_changed: session = scoped_session(self.session_factory) try: - self.saveBidInSession(bid_id, bid, session) - session.commit() - if bid.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED: + activate_bid = False + if offer.swap_type == SwapTypes.BUYER_FIRST: + if bid.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED: + activate_bid = True + else: + raise ValueError('TODO') + + if activate_bid: self.activateBid(session, bid) else: - self.deactivateBid(offer, bid) + self.deactivateBid(session, offer, bid) + + self.saveBidInSession(bid_id, bid, session) + session.commit() finally: session.close() session.remove() @@ -4222,8 +4279,9 @@ class BasicSwap(BaseApp): def getWalletInfo(self, coin): - blockchaininfo = self.coin_clients[coin]['interface'].getBlockchainInfo() - walletinfo = self.coin_clients[coin]['interface'].getWalletInfo() + ci = self.ci(coin) + blockchaininfo = ci.getBlockchainInfo() + walletinfo = ci.getWalletInfo() scale = chainparams[coin]['decimal_places'] rv = { @@ -4234,6 +4292,7 @@ class BasicSwap(BaseApp): 'balance': format_amount(make_int(walletinfo['balance'], scale), scale), 'unconfirmed': format_amount(make_int(walletinfo.get('unconfirmed_balance'), scale), scale), 'synced': '{0:.2f}'.format(round(blockchaininfo['verificationprogress'], 2)), + 'expected_seed': ci.knownWalletSeed(), } return rv diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index b78f430..77e247f 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -199,6 +199,9 @@ chainparams = { class CoinInterface: + def __init__(self): + self._unknown_wallet_seed = True + def format_amount(self, amount_int): return format_amount(amount_int, self.exp()) @@ -207,3 +210,9 @@ class CoinInterface: def ticker(self): return chainparams[self.coin_type()]['ticker'] + + def setWalletSeedWarning(self, value): + self._unknown_wallet_seed = value + + def knownWalletSeed(self): + return not self._unknown_wallet_seed diff --git a/basicswap/http_server.py b/basicswap/http_server.py index 486eb06..808a387 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -254,6 +254,7 @@ class HttpHandler(BaseHTTPRequestHandler): 'blocks': w['blocks'], 'synced': w['synced'], 'deposit_address': w['deposit_address'], + 'expected_seed': w['expected_seed'], }) if float(w['unconfirmed']) > 0.0: wallets_formatted[-1]['unconfirmed'] = w['unconfirmed'] diff --git a/basicswap/interface_btc.py b/basicswap/interface_btc.py index b5d7b78..20301ad 100644 --- a/basicswap/interface_btc.py +++ b/basicswap/interface_btc.py @@ -118,6 +118,7 @@ class BTCInterface(CoinInterface): return abs(a - b) < 20 def __init__(self, coin_settings, network): + super().__init__() self.rpc_callback = make_rpc_func(coin_settings['rpcport'], coin_settings['rpcauth']) self.txoType = CTxOut self._network = network @@ -145,6 +146,9 @@ class BTCInterface(CoinInterface): def getWalletInfo(self): return self.rpc_callback('getwalletinfo') + def getWalletSeedID(self): + return self.rpc_callback('getwalletinfo')['hdseedid'] + def getNewAddress(self, use_segwit): args = ['swap_receive'] if use_segwit: @@ -177,6 +181,10 @@ class BTCInterface(CoinInterface): def getPubkey(self, privkey): return PublicKey.from_secret(privkey).format() + def getAddressHashFromKey(self, key): + pk = self.getPubkey(key) + return hash160(pk) + def verifyKey(self, k): i = b2i(k) return(i < ep.o and i > 0) diff --git a/basicswap/interface_part.py b/basicswap/interface_part.py index a8cbb29..b725e64 100644 --- a/basicswap/interface_part.py +++ b/basicswap/interface_part.py @@ -33,6 +33,10 @@ class PARTInterface(BTCInterface): self._network = network self.blocks_confirmed = coin_settings['blocks_confirmed'] + def knownWalletSeed(self): + # TODO: Double check + return True + def getNewAddress(self, use_segwit): return self.rpc_callback('getnewaddress', ['swap_receive']) diff --git a/basicswap/interface_xmr.py b/basicswap/interface_xmr.py index 159bb5a..d52ae0e 100644 --- a/basicswap/interface_xmr.py +++ b/basicswap/interface_xmr.py @@ -54,6 +54,7 @@ class XMRInterface(CoinInterface): return 32 def __init__(self, coin_settings, network): + super().__init__() rpc_cb = make_xmr_rpc_func(coin_settings['rpcport']) rpc_wallet_cb = make_xmr_wallet_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth']) @@ -154,6 +155,11 @@ class XMRInterface(CoinInterface): def getPubkey(self, privkey): return ed25519_get_pubkey(privkey) + def getAddressFromKeys(self, key_view, key_spend): + pk_view = self.getPubkey(key_view) + pk_spend = self.getPubkey(key_spend) + return xmr_util.encode_address(pk_view, pk_spend) + def verifyKey(self, k): i = b2i(k) return(i < edf.l and i > 8) diff --git a/basicswap/templates/wallets.html b/basicswap/templates/wallets.html index ee12cd3..29089e5 100644 --- a/basicswap/templates/wallets.html +++ b/basicswap/templates/wallets.html @@ -21,6 +21,7 @@ Balance:{{ w.balance }}{% if w.unconfirmed %}Unconfirmed:{{ w.unconfirmed }}{% endif %} Blocks:{{ w.blocks }} Synced:{{ w.synced }} +Expected Seed:{{ w.expected_seed }} {{ w.deposit_address }} Amount: Address: Subtract fee: Fee Rate:{{ w.fee_rate }}Est Fee:{{ w.est_fee }} @@ -32,4 +33,4 @@

home

- \ No newline at end of file + diff --git a/doc/notes.md b/doc/notes.md index 057fef5..7b0f4c2 100644 --- a/doc/notes.md +++ b/doc/notes.md @@ -4,3 +4,56 @@ ``` python setup.py test -s tests.basicswap.test_xmr.Test.test_02_leader_recover_a_lock_tx ``` + +## TODO +Features still required (of many): + - Cached addresses must be regenerated after use. + - Option to lookup data from public explorers / nodes. + - Ability to swap coin-types without running nodes for all coin-types + - More swap protocols + - Method to load mnemonic into Particl. + - Load seeds for other wallets from same mnemonic. + - COIN must be defined per coin. + + +## Seller first protocol: + +Seller sends the 1st transaction. + +1. Seller posts offer. + - smsg from seller to network + coin-from + coin-to + amount-from + rate + min-amount + time-valid + +2. Buyer posts bid: + - smsg from buyer to seller + offerid + amount + proof-of-funds + address_to_buyer + time-valid + +3. Seller accepts bid: + - verifies proof-of-funds + - generates secret + - submits initiate tx to coin-from network + - smsg from seller to buyer + txid + initiatescript (includes pkhash_to_seller as the pkhash_refund) + +4. Buyer participates: + - inspects initiate tx in coin-from network + - submits participate tx in coin-to network + +5. Seller redeems: + - constructs participatescript + - inspects participate tx in coin-to network + - redeems from participate tx revealing secret + +6. Buyer redeems: + - scans coin-to network for seller-redeem tx + - redeems from initiate tx with revealed secret diff --git a/tests/basicswap/test_wallet_init.py b/tests/basicswap/test_wallet_init.py index b8246e4..f055c8a 100644 --- a/tests/basicswap/test_wallet_init.py +++ b/tests/basicswap/test_wallet_init.py @@ -178,14 +178,22 @@ class Test(unittest.TestCase): try: waitForServer(12700) - wallets = json.loads(urlopen('http://localhost:12700/json/wallets').read()) - print('[rm] wallets', dumpj(wallets)) + wallets_0 = json.loads(urlopen('http://localhost:12700/json/wallets').read()) + print('[rm] wallets_0', dumpj(wallets_0)) + assert(wallets_0['1']['expected_seed'] == True) + assert(wallets_0['6']['expected_seed'] == True) waitForServer(12701) - wallets = json.loads(urlopen('http://localhost:12701/json/wallets').read()) - print('[rm] wallets', dumpj(wallets)) + wallets_1 = json.loads(urlopen('http://localhost:12701/json/wallets').read()) + print('[rm] wallets_1', dumpj(wallets_1)) - raise ValueError('TODO') + assert(wallets_0['1']['expected_seed'] == True) + assert(wallets_1['6']['expected_seed'] == True) + + # TODO: Check other coins + + assert(wallets_0['1']['deposit_address'] == wallets_1['1']['deposit_address']) + assert(wallets_0['6']['deposit_address'] == wallets_1['6']['deposit_address']) except Exception: traceback.print_exc()