Display warning when wallet seedid doesn't match expected.

2024-05-20_merge
tecnovert 4 years ago
parent 5a163e0f86
commit e7afd5e67d
No known key found for this signature in database
GPG Key ID: 8ED6D8750C4E3F93
  1. 52
      README.md
  2. 145
      basicswap/basicswap.py
  3. 9
      basicswap/chainparams.py
  4. 1
      basicswap/http_server.py
  5. 8
      basicswap/interface_btc.py
  6. 4
      basicswap/interface_part.py
  7. 6
      basicswap/interface_xmr.py
  8. 3
      basicswap/templates/wallets.html
  9. 53
      doc/notes.md
  10. 18
      tests/basicswap/test_wallet_init.py

@ -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)

@ -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

@ -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

@ -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']

@ -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)

@ -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'])

@ -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)

@ -21,6 +21,7 @@
<tr><td>Balance:</td><td>{{ w.balance }}</td>{% if w.unconfirmed %}<td>Unconfirmed:</td><td>{{ w.unconfirmed }}</td>{% endif %}</tr>
<tr><td>Blocks:</td><td>{{ w.blocks }}</td></tr>
<tr><td>Synced:</td><td>{{ w.synced }}</td></tr>
<tr><td>Expected Seed:</td><td>{{ w.expected_seed }}</td></tr>
<tr><td><input type="submit" name="newaddr_{{ w.cid }}" value="Deposit Address"></td><td>{{ w.deposit_address }}</td></tr>
<tr><td><input type="submit" name="withdraw_{{ w.cid }}" value="Withdraw"></td><td>Amount: <input type="text" name="amt_{{ w.cid }}"></td><td>Address: <input type="text" name="to_{{ w.cid }}"></td><td>Subtract fee: <input type="checkbox" name="subfee_{{ w.cid }}"></td></tr>
<tr><td>Fee Rate:</td><td>{{ w.fee_rate }}</td><td>Est Fee:</td><td>{{ w.est_fee }}</td></tr>
@ -32,4 +33,4 @@
</form>
<p><a href="/">home</a></p>
</body></html>
</body></html>

@ -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

@ -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()

Loading…
Cancel
Save