test client: factor out wallet sync API into class

In preparation for adding the LBRY SDK api class
This commit is contained in:
Daniel Krol 2022-06-10 21:07:55 -04:00
parent 66e5cf7fe7
commit d467559dcd
4 changed files with 159 additions and 107 deletions

View file

@ -103,7 +103,7 @@ func getPostData(w http.ResponseWriter, req *http.Request, reqStruct PostRequest
} }
if err := json.NewDecoder(req.Body).Decode(&reqStruct); err != nil { if err := json.NewDecoder(req.Body).Decode(&reqStruct); err != nil {
errorJson(w, http.StatusBadRequest, "Malformed request body JSON") errorJson(w, http.StatusBadRequest, "Request body JSON malformed or structure mismatch")
return false return false
} }

View file

@ -25,9 +25,9 @@ Now that the account exists, grab an auth token with both clients.
``` ```
>>> c1.get_auth_token() >>> c1.get_auth_token()
Got auth token: a489d5cacc0a3db4811c34d203683482d90c605b03ae007fa5ae32ef17252bd9 Got auth token: b646a357038d394ef7b70f350c666aeb29e24072c7eeaaad4fb6759c1fca281a
>>> c2.get_auth_token() >>> c2.get_auth_token()
Got auth token: 1fe687db8ab493ed260f499b674cfa49edefd3c03a718905c62d3f850dc50567 Got auth token: bd24ae67cb1bde2da8a27f2c2e5ec8ff1f9bebccb11e16dd35aea31bf422133c
``` ```
## Syncing ## Syncing
@ -38,7 +38,7 @@ Note that after POSTing, it says it "got" a new wallet. This is because the POST
``` ```
>>> c1.new_wallet_state() >>> c1.new_wallet_state()
>>> c1.post_wallet() >>> c1.update_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Got new walletState:
WalletState(sequence=1, encrypted_wallet='-') WalletState(sequence=1, encrypted_wallet='-')
@ -57,7 +57,7 @@ WalletState(sequence=1, encrypted_wallet='-')
Push a new version, GET it with the other client. Even though we haven't edited the encrypted wallet yet, we can still increment the sequence number. Push a new version, GET it with the other client. Even though we haven't edited the encrypted wallet yet, we can still increment the sequence number.
``` ```
>>> c2.post_wallet() >>> c2.update_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Got new walletState:
WalletState(sequence=2, encrypted_wallet='-') WalletState(sequence=2, encrypted_wallet='-')
@ -75,21 +75,21 @@ For demo purposes, this test client represents each change to the wallet by appe
'-' '-'
>>> c1.change_encrypted_wallet() >>> c1.change_encrypted_wallet()
>>> c1.cur_encrypted_wallet() >>> c1.cur_encrypted_wallet()
'-:cfF6' '-:a776'
``` ```
The wallet is synced between the clients. The wallet is synced between the clients.
``` ```
>>> c1.post_wallet() >>> c1.update_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Got new walletState:
WalletState(sequence=3, encrypted_wallet='-:cfF6') WalletState(sequence=3, encrypted_wallet='-:a776')
>>> c2.get_wallet() >>> c2.get_wallet()
Got latest walletState: Got latest walletState:
WalletState(sequence=3, encrypted_wallet='-:cfF6') WalletState(sequence=3, encrypted_wallet='-:a776')
>>> c2.cur_encrypted_wallet() >>> c2.cur_encrypted_wallet()
'-:cfF6' '-:a776'
``` ```
## Merging Changes ## Merging Changes
@ -100,44 +100,44 @@ Both clients create changes. They now have diverging wallets.
>>> c1.change_encrypted_wallet() >>> c1.change_encrypted_wallet()
>>> c2.change_encrypted_wallet() >>> c2.change_encrypted_wallet()
>>> c1.cur_encrypted_wallet() >>> c1.cur_encrypted_wallet()
'-:cfF6:565b' '-:a776:8fc8'
>>> c2.cur_encrypted_wallet() >>> c2.cur_encrypted_wallet()
'-:cfF6:6De1' '-:a776:2433'
``` ```
One client POSTs its change first. One client POSTs its change first.
``` ```
>>> c1.post_wallet() >>> c1.update_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Got new walletState:
WalletState(sequence=4, encrypted_wallet='-:cfF6:565b') WalletState(sequence=4, encrypted_wallet='-:a776:8fc8')
``` ```
The other client pulls that change, and _merges_ those changes on top of the changes it had saved locally. The other client pulls that change, and _merges_ those changes on top of the changes it had saved locally.
The _merge base_ that a given client uses is the last version that it successfully got from or POSTed to the server. You can see the merge base here: `"-:cfF6"`, the first part of the wallet which both clients had in common before the merge. The _merge base_ that a given client uses is the last version that it successfully got from or POSTed to the server. You can see the merge base here: `"-:a776"`, the first part of the wallet which both clients had in common before the merge.
``` ```
>>> c2.get_wallet() >>> c2.get_wallet()
Got latest walletState: Got latest walletState:
WalletState(sequence=4, encrypted_wallet='-:cfF6:565b') WalletState(sequence=4, encrypted_wallet='-:a776:8fc8')
>>> c2.cur_encrypted_wallet() >>> c2.cur_encrypted_wallet()
'-:cfF6:565b:6De1' '-:a776:8fc8:2433'
``` ```
Finally, the client with the merged wallet pushes it to the server, and the other client GETs the update. Finally, the client with the merged wallet pushes it to the server, and the other client GETs the update.
``` ```
>>> c2.post_wallet() >>> c2.update_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Got new walletState:
WalletState(sequence=5, encrypted_wallet='-:cfF6:565b:6De1') WalletState(sequence=5, encrypted_wallet='-:a776:8fc8:2433')
>>> c1.get_wallet() >>> c1.get_wallet()
Got latest walletState: Got latest walletState:
WalletState(sequence=5, encrypted_wallet='-:cfF6:565b:6De1') WalletState(sequence=5, encrypted_wallet='-:a776:8fc8:2433')
>>> c1.cur_encrypted_wallet() >>> c1.cur_encrypted_wallet()
'-:cfF6:565b:6De1' '-:a776:8fc8:2433'
``` ```
## Conflicts ## Conflicts
@ -148,22 +148,22 @@ A client cannot POST if it is not up to date. It needs to merge in any new chang
``` ```
>>> c2.change_encrypted_wallet() >>> c2.change_encrypted_wallet()
>>> c2.post_wallet() >>> c2.update_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Got new walletState:
WalletState(sequence=6, encrypted_wallet='-:cfF6:565b:6De1:053a') WalletState(sequence=6, encrypted_wallet='-:a776:8fc8:2433:0FdD')
>>> c1.change_encrypted_wallet() >>> c1.change_encrypted_wallet()
>>> c1.post_wallet() >>> c1.update_wallet()
Wallet state out of date. Getting updated wallet state. Try posting again after this. Wallet state out of date. Getting updated wallet state. Try posting again after this.
Got new walletState: Got new walletState:
WalletState(sequence=6, encrypted_wallet='-:cfF6:565b:6De1:053a') WalletState(sequence=6, encrypted_wallet='-:a776:8fc8:2433:0FdD')
``` ```
Now the merge is complete, and the client can make a second POST request containing the merged wallet. Now the merge is complete, and the client can make a second POST request containing the merged wallet.
``` ```
>>> c1.post_wallet() >>> c1.update_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Got new walletState:
WalletState(sequence=7, encrypted_wallet='-:cfF6:565b:6De1:053a:6774') WalletState(sequence=7, encrypted_wallet='-:a776:8fc8:2433:0FdD:BA43')
``` ```

View file

@ -59,7 +59,7 @@ Note that after POSTing, it says it "got" a new wallet. This is because the POST
code_block(""" code_block("""
c1.new_wallet_state() c1.new_wallet_state()
c1.post_wallet() c1.update_wallet()
""") """)
print(""" print("""
@ -77,7 +77,7 @@ Push a new version, GET it with the other client. Even though we haven't edited
""") """)
code_block(""" code_block("""
c2.post_wallet() c2.update_wallet()
c1.get_wallet() c1.get_wallet()
""") """)
@ -98,7 +98,7 @@ The wallet is synced between the clients.
""") """)
code_block(""" code_block("""
c1.post_wallet() c1.update_wallet()
c2.get_wallet() c2.get_wallet()
c2.cur_encrypted_wallet() c2.cur_encrypted_wallet()
""") """)
@ -123,7 +123,7 @@ One client POSTs its change first.
""") """)
code_block(""" code_block("""
c1.post_wallet() c1.update_wallet()
""") """)
print(""" print("""
@ -142,7 +142,7 @@ Finally, the client with the merged wallet pushes it to the server, and the othe
""") """)
code_block(""" code_block("""
c2.post_wallet() c2.update_wallet()
c1.get_wallet() c1.get_wallet()
c1.cur_encrypted_wallet() c1.cur_encrypted_wallet()
""") """)
@ -157,9 +157,9 @@ A client cannot POST if it is not up to date. It needs to merge in any new chang
code_block(""" code_block("""
c2.change_encrypted_wallet() c2.change_encrypted_wallet()
c2.post_wallet() c2.update_wallet()
c1.change_encrypted_wallet() c1.change_encrypted_wallet()
c1.post_wallet() c1.update_wallet()
""") """)
print(""" print("""
@ -167,5 +167,5 @@ Now the merge is complete, and the client can make a second POST request contain
""") """)
code_block(""" code_block("""
c1.post_wallet() c1.update_wallet()
""") """)

View file

@ -5,18 +5,101 @@ from pprint import pprint
CURRENT_VERSION = 1 CURRENT_VERSION = 1
WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet'])
class WalletSync():
BASE_URL = 'http://localhost:8090' BASE_URL = 'http://localhost:8090'
AUTH_URL = BASE_URL + '/auth/full' AUTH_URL = BASE_URL + '/auth/full'
REGISTER_URL = BASE_URL + '/signup' REGISTER_URL = BASE_URL + '/signup'
WALLET_URL = BASE_URL + '/wallet' WALLET_URL = BASE_URL + '/wallet'
@classmethod
def register(cls, email, password):
body = json.dumps({
'email': email,
'password': password,
})
response = requests.post(cls.REGISTER_URL, body)
if response.status_code != 201:
print ('Error', response.status_code)
print (response.content)
return False
return True
@classmethod
def get_auth_token(cls, email, password, device_id):
body = json.dumps({
'email': email,
'password': password,
'deviceId': device_id,
})
response = requests.post(cls.AUTH_URL, body)
if response.status_code != 200:
print ('Error', response.status_code)
print (response.content)
return None
return response.json()['token']
@classmethod
def get_wallet(cls, token):
params = {
'token': token,
}
response = requests.get(cls.WALLET_URL, params=params)
# TODO check response version on client side now
if response.status_code != 200:
print ('Error', response.status_code)
print (response.content)
return None, None
wallet_state = WalletState(
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'],
)
hmac = response.json()['hmac']
return wallet_state, hmac
@classmethod
def update_wallet(cls, wallet_state, hmac, token):
body = json.dumps({
'version': CURRENT_VERSION,
'token': token,
"encryptedWallet": wallet_state.encrypted_wallet,
"sequence": wallet_state.sequence,
"hmac": hmac,
})
response = requests.post(cls.WALLET_URL, body)
# TODO check that response.json().version == CURRENT_VERSION
if response.status_code == 200:
conflict = False
print ('Successfully updated wallet state on server')
elif response.status_code == 409:
conflict = True
print ('Wallet state out of date. Getting updated wallet state. Try posting again after this.')
# Not an error! We still want to merge in the returned wallet.
else:
print ('Error', response.status_code)
print (response.content)
return None, None, None
wallet_state = WalletState(
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'],
)
hmac = response.json()['hmac']
return wallet_state, hmac, conflict
# TODO - We should have: # TODO - We should have:
# * self.last_synced_wallet_state - as described # * self.last_synced_wallet_state - as described
# * self.current_wallet_state - WalletState(cur_encrypted_wallet(), sequence + 1) - and current_wallet_state # * self.current_wallet_state - WalletState(cur_encrypted_wallet(), sequence + 1) - and current_wallet_state
# We don't need it yet but we'd be avoiding the entire point of the syncing system. At least keep it around in this demo. # We don't need it yet but we'd be avoiding the entire point of the syncing system. At least keep it around in this demo.
WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet'])
# TODO - do this correctly. This is a hack example. # TODO - do this correctly. This is a hack example.
def derive_login_password(root_password): def derive_login_password(root_password):
return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[:10] return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[:10]
@ -88,29 +171,22 @@ class Client():
self.root_password = root_password self.root_password = root_password
def register(self): def register(self):
body = json.dumps({ success = WalletSync.register(
'email': self.email, self.email,
'password': derive_login_password(self.root_password), derive_login_password(self.root_password),
}) )
response = requests.post(REGISTER_URL, body) if success:
if response.status_code != 201:
print ('Error', response.status_code)
print (response.content)
return
print ("Registered") print ("Registered")
def get_auth_token(self): def get_auth_token(self):
body = json.dumps({ token = WalletSync.get_auth_token(
'email': self.email, self.email,
'password': derive_login_password(self.root_password), derive_login_password(self.root_password),
'deviceId': self.device_id, self.device_id,
}) )
response = requests.post(AUTH_URL, body) if not token:
if response.status_code != 200:
print ('Error', response.status_code)
print (response.content)
return return
self.auth_token = json.loads(response.content)['token'] self.auth_token = token
print ("Got auth token: ", self.auth_token) print ("Got auth token: ", self.auth_token)
# TODO - What about cases where we are managing multiple different wallets? # TODO - What about cases where we are managing multiple different wallets?
@ -119,31 +195,22 @@ class Client():
# want to annotate them with which account we're talking about. Again, we # want to annotate them with which account we're talking about. Again, we
# should see how LBRY Desktop/SDK deal with it. # should see how LBRY Desktop/SDK deal with it.
def get_wallet(self): def get_wallet(self):
params = { new_wallet_state, hmac = WalletSync.get_wallet(self.auth_token)
'token': self.auth_token,
} # If there was a failure
response = requests.get(WALLET_URL, params=params) if not new_wallet_state:
if response.status_code != 200:
# TODO check response version on client side now
print ('Error', response.status_code)
print (response.content)
return return
hmac_key = derive_hmac_key(self.root_password) hmac_key = derive_hmac_key(self.root_password)
new_wallet_state = WalletState(
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'],
)
hmac = response.json()['hmac']
if not check_hmac(new_wallet_state, hmac_key, hmac): if not check_hmac(new_wallet_state, hmac_key, hmac):
print ('Error - bad hmac on new wallet') print ('Error - bad hmac on new wallet')
print (response.content) print (new_wallet_state, hmac)
return return
if self.wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state): if self.wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state):
print ('Error - new wallet does not validate') print ('Error - new wallet does not validate')
print (response.content) print ('current:', self.wallet_state)
print ('got:', new_wallet_state)
return return
if self.wallet_state is None: if self.wallet_state is None:
@ -156,7 +223,7 @@ class Client():
print ("Got latest walletState:") print ("Got latest walletState:")
pprint(self.wallet_state) pprint(self.wallet_state)
def post_wallet(self): def update_wallet(self):
# Create a *new* wallet state, indicating that it was last updated by this # Create a *new* wallet state, indicating that it was last updated by this
# device, with the updated sequence, and include our local encrypted wallet changes. # device, with the updated sequence, and include our local encrypted wallet changes.
# Don't set self.wallet_state to this until we know that it's accepted by # Don't set self.wallet_state to this until we know that it's accepted by
@ -171,45 +238,30 @@ class Client():
encrypted_wallet=self.cur_encrypted_wallet(), encrypted_wallet=self.cur_encrypted_wallet(),
sequence=self.wallet_state.sequence + 1 sequence=self.wallet_state.sequence + 1
) )
wallet_request = { hmac = create_hmac(submitted_wallet_state, hmac_key)
'version': CURRENT_VERSION,
'token': self.auth_token,
"encryptedWallet": submitted_wallet_state.encrypted_wallet,
"sequence": submitted_wallet_state.sequence,
"hmac": create_hmac(submitted_wallet_state, hmac_key),
}
response = requests.post(WALLET_URL, json.dumps(wallet_request)) # Submit our wallet, get the latest wallet back as a response
new_wallet_state, new_hmac, conflict = WalletSync.update_wallet(submitted_wallet_state, hmac, self.auth_token)
if response.status_code == 200: # If there was a failure (not just a conflict)
# TODO check response version on client side now if not new_wallet_state:
# Our local changes are no longer local, so we reset them
self._encrypted_wallet_local_changes = ''
print ('Successfully updated wallet state on server')
elif response.status_code == 409:
print ('Wallet state out of date. Getting updated wallet state. Try posting again after this.')
# Don't return yet! We got the updated state here, so we still process it below.
else:
print ('Error', response.status_code)
print (response.content)
return return
# Now we get a new wallet back as a response # If there's not a conflict, we submitted successfully and should reset our previously local changes
# TODO - factor this code into the same thing as the get_wallet function if not conflict:
self._encrypted_wallet_local_changes = ''
new_wallet_state = WalletState( # TODO - there's some code in common here with the get_wallet function. factor it out.
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'], if not check_hmac(new_wallet_state, hmac_key, new_hmac):
)
hmac = response.json()['hmac']
if not check_hmac(new_wallet_state, hmac_key, hmac):
print ('Error - bad hmac on new wallet') print ('Error - bad hmac on new wallet')
print (response.content) print (new_wallet_state, hmac)
return return
if submitted_wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state): if submitted_wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state):
print ('Error - new wallet does not validate') print ('Error - new wallet does not validate')
print (response.content) print ('current:', self.wallet_state)
print ('got:', new_wallet_state)
return return
self.wallet_state = new_wallet_state self.wallet_state = new_wallet_state