Toycoin Part 4: Wallets
Note: the toycoin series of posts is for learning / illustrative purposes only; no part of it should be considered secure or useful for real-world purposes.
The previous post described transactions and tokens.
Clients that store tokens and interact with the blockchain via transactions are wallets. We can imagine the simplest wallet as storing its address (public key), its private key for signing transactions, and the tokens that it holds.
class Wallet:
"""Wallet, initialized with owner's RSA keys."""
def __init__(self,
public_key: bytes,
private_key: rsa.RSAPrivateKey):
self.wallet : List[transaction.Token] = []
self.pending : List[Tuple[bytes, transaction.Token]] = []
self.public_key = public_key
self.private_key = private_key
def balance(self) -> int:
"""Return current wallet balance (exclude pending)."""
return sum(token['value'] for token in self.wallet)
Wallets submit transactions to the network when they want to send tokens to other wallets. Since the blockchain protocotol needs to confirm the transactions (by including them in blocks), the wallet includes a pending
store.
Sending is a matter of finding enough tokens and calling the send
interface defined in transactions
. For now, the actual sending – the networking component – is ignored.
def send(self,
send_value: int,
receiver: bytes
) -> Optional[Tuple[List[transaction.Token],
transaction.Transaction]]:
"""Attempt to generate transaction that sends value.
Tokens included in the transaction are placed in pending state.
"""
if send_value > self.balance():
return None
# FIFO
sum_value, i = 0, 0
while sum_value < send_value:
sum_value += self.wallet[i]['value']
i += 1
tokens, txn = transaction.send(receiver,
self.public_key,
self.private_key,
send_value,
self.wallet[:i])
self.pending.append((transaction.hash_txn(txn), self.wallet[:i]))
self.wallet = self.wallet[i:]
return tokens, txn
Since send
places tokens in the pending
store, we can imagine that network messages will trigger some sort of resolution:
def confirm_send(self, txn_hash: bytes):
"""Remove confirmed transaction from pending state."""
self.pending = [(h, tokens) for h, tokens in self.pending
if h != txn_hash]
def reject_send(self, txn_hash: bytes):
"""Return tokens to wallet from pending state."""
pending = []
for h, tokens in self.pending:
if h == txn_hash:
self.wallet = tokens + self.wallet
else:
pending.append((h, tokens))
self.pending = pending
There is some iterating required, if we imagine that multiple transacitons may be pending at any given time.
As for receiving tokens, we merely need to check if we are receiving tokens from another wallet, or from ourselves (in the case of getting change back).
def receive(self, txn: transaction.Transaction):
"""Add tokens to wallet."""
if txn is None:
return
txn_hash = transaction.hash_txn(txn)
if self.public_key == txn['receiver']:
self.wallet.append({'txn_hash': txn_hash,
'owner': self.public_key,
'value': txn['receiver_value'],
'signature': txn['receiver_signature']})
elif self.public_key == txn['sender']:
self.wallet.append({'txn_hash': txn_hash,
'owner': self.public_key,
'value': txn['sender_change'],
Note that no logic for validating the received transactions is included. It’s assumed that the network will only broadcast validated transactions (e.g. as completed blocks). Additionally, wallets can include additional functionality to request the Merkle hash paths for transactions in prior blocks – to be addressed later.
Testing
We can set up some wallets and genesis transactions, and test the wallet and transaction modules together.
def test_send_receive(self):
"""Test sending and receiving tokens via transactions."""
a_wallet, b_wallet = gen_wallet(), gen_wallet()
c_wallet, d_wallet = gen_wallet(), gen_wallet()
txn0a = {'previous_hashes': [],
'receiver': a_wallet.public_key,
'receiver_value': 100,
'receiver_signature': b'',
'sender': b'genesis',
'sender_change': 0,
'sender_signature': b''
}
txn0b = {'previous_hashes': [],
'receiver': a_wallet.public_key,
'receiver_value': 50,
'receiver_signature': b'',
'sender': b'genesis',
'sender_change': 0,
'sender_signature': b''
}
# genesis receive (the genesis txn is not valid)
assert transaction.valid_txn([], txn0a) is False
assert transaction.valid_txn([], txn0b) is False
assert a_wallet.balance() == 0
a_wallet.receive(txn0a)
assert a_wallet.balance() == 100
a_wallet.receive(txn0b)
assert a_wallet.balance() == 150
assert transaction.valid_token(txn0a, a_wallet.wallet[0])
assert transaction.valid_token(txn0b, a_wallet.wallet[1])
And start to iterate through a chain of transactions, verifying wallet state as various send and receive actions occur.
# cannot send more than wallet total
assert a_wallet.send(200, b_wallet.public_key) is None
# A sends first token to B, with 50 in change (txn pending)
_, txn1 = a_wallet.send(50, b_wallet.public_key)
assert a_wallet.balance() == 50
# rejecting the send restores A wallet
assert len(a_wallet.pending) == 1
a_wallet.reject_send(transaction.hash_txn(txn1))
assert a_wallet.balance() == 150
assert len(a_wallet.wallet) == 2
assert len(a_wallet.pending) == 0
# send again and confirm for A and B
_, txn1 = a_wallet.send(50, b_wallet.public_key)
a_wallet.confirm_send(transaction.hash_txn(txn1))
assert a_wallet.balance() == 50
assert a_wallet.pending == []
a_wallet.receive(txn1)
assert a_wallet.balance() == 100
b_wallet.receive(txn1)
assert b_wallet.balance() == 50
The testing works through a few more scenarios, abbreviated here but available in the source.
Incidentally, pytest-cov
shows good coverage so far, though high coverage shouldn’t be taken as infallibility.
---------- coverage: platform darwin, python 3.9.1-final-0 -----------
Name Stmts Miss Cover
--------------------------------------------
toycoin/hash.py 6 0 100%
toycoin/merkle.py 101 10 90%
toycoin/signature.py 26 0 100%
toycoin/transaction.py 45 1 98%
toycoin/utils.py 6 0 100%
toycoin/wallet.py 39 2 95%
--------------------------------------------
Wrapping Up
The wallet module was written in tandem with the revised transaction model, since it was a lot easier to imagine how transactions would work with a consumer of the interface.
It’s been a bit tricky to come up with these implementations, even though they are conceptually simple. Maybe it’s due to a few more considerations than usual:
-
the functions are pure so far, but they need to play nicely with stateful actions and data eventually (i.e. interacting with the blockchain network and looking up previous blocks or transactions for verification, etc)
-
the implementation and testing primarily focuses on correctness given honest actors, but it should also deal with bad actors in the way the the protocol specifies (i.e. trying not to introduce unintended vulnerabilities)
There’s probably a lot of detail and nuance that’s missing, but taking the first steps is the point of this exercise.
In such exercises – and in life at large – I’m often reminded of John Salvatier’s essay, “reality has a surprising amount of detail”.