Decentralized currency
In this module, you will learn how we can leverage the architecture described in the previous module, to support a currency with all the benefits of decentralization.
Accounts and transfers
For our community to be able to manage its own currency, we need to support at least a few basic features.
We need each member to have:
- A unique account identifier, or address.
- Some amount of currency it possesses, the balance of the account
To use it, a member needs, to be able to, at the minimum:
- Transfer some currency from their account to another member's account.
- Receive currency from another member.
On Mavryk, the native currency is called mav.
How would you implement this for our community?
Centralized approach
We could use a centralized approach by appointing one of the members of the community as a banker.
The banker would keep a ledger of all transactions it processes. They would also keep track of the current balance of each account.
Whenever a member requests a transfer:
- the banker checks their identity
- they check that the balance is sufficient
- they reduce the balance of the source, and increase the one from the destination
- they charge for this service
However, similarly to a hosting company, such a banker may:
- be unavailable, or go bankrupt
- block some transfers
- create fake transfers, or other illegal use of the funds
Again, using such a centralized approach means users have to trust a single entity that has full control over the funds. This can be very dangerous.
Decentralized approach
Think about how our community could maintain the balances of accounts and handle transfers, without having to trust a single entity.
We could build on top of our p2p data sharing networks, and have each node contribute equally to supporting this currency.
A node would have to be able to:
- Receive and emit transactions
- Keep track of the balance of each account
- Check the validity of these transactions
- Authenticate their author
- Check that the balance is sufficient
How can we do this?
Receiving and emitting transactions could be handled in the same way as receiving and emitting files in our p2p network.
When a node receives a transaction:
- It performs some verification (to avoid DOS attacks)
- It propagates it to its neighbors
Maintaining the balance of each address could be done by each node based on the transactions it receives. For each member's address (unique identifier), each node can store a corresponding balance.
Once a transaction is validated and confirmed, the node will need to:
- Subtract the transferred amount from the balance of the source address
- Add this amount to the balance of the destination address
Authentication
To validate a transaction, the node first needs to authenticate its source.
For example, let's take the following transaction:
Alice transfers 50 mav to Bob
Before the node subtracts 50 mav from Alice's account and adds 50 mav to Bob’s account, it has to check that Alice is the author of this transaction.
This can be done using asymmetric cryptography. Each member generates and stores:
A private key, that only they possess, and allows them to produce a digital signature for a certain piece of data: “I, Alice, certify that for my transaction #284, I transfer 50 mav to Bob”
A public key, which they share with the whole network, and allows:
- For the user to uniquely identify themselves (their account)
- For any member, to verify that they are the author of a signed message
Mavryk users use a wallet (Kukai, Umami, Mavryk Wallet, Ledger, …) to generate and store their private keys and sign transactions.
In practice, the address of an account is based on the hash of the public key of the holder of that account.
Checking that the balance is sufficient
At first, it may seem like all a node needs to do when receiving a transaction, after authenticating its author, is to check that the balance of the account of the source of the funds is more than the amount it transfers to the destination.
In a decentralized network, however, it is not that simple.
Let's assume that we already found a way to make sure every node in the p2p network receives every transaction.
A key remaining issue is that the validity of transactions may depend on the order in which a given node receives them.
Assume that at the start, Alice has 100 Mav and Bob has 10 Mav. We have 2 transactions:
- Transaction A: Alice transfers 50 Mav to Bob
- Transaction B: Bob transfers 30 Mav to Carl
If a node applies A first, Bob’s balance becomes 60 mav. B is valid and can be executed.
If a node applies B first, B is invalid: Bob’s balance is 10 Mav, which is too low for a transfer of 30 Mav.
These two nodes end up in a different state!
As all nodes should agree on the balance of each account, they not only need to agree on which transactions need to be performed, but also on their order.
A key aspect of managing a currency on a decentralized network is therefore to agree on which transactions to add, and in which order.
Ordering transactions and associated issues
Assuming again that each node receives every transaction sent to a node by their author, how would you make sure every node executes them in the same order?
One natural idea would be to attach a precise timestamp to every transaction, then have nodes execute transactions in the corresponding order.
Can you think of any issues with that approach?
One issue is the case where two transactions have the same timestamp. This could, however, easily be resolved by sorting these transactions using their hash, which is unique.
Another issue is that to execute a given transaction, you would need to make sure you already received every transaction with a smaller or equal timestamp.
One could consider allowing for some set delay to account for network issues that could slow the propagation of some transactions. However, if nodes reject transactions that arrive too late compared to their attached timestamp, two nodes may reject different transactions and therefore end up in different incompatible states.
Another approach could be for nodes to revert to a previous state, whenever a transaction arrives after the application of transactions with a higher timestamp, then reapply the transactions in the right order to get the new updated state. Assuming that all nodes eventually receive all transactions, they would always converge to the same state.
This, however, means that anyone who had inquired about the state from such a node before a revert, would have obtained inaccurate information, and potentially made important decisions based on that information.
Double spending example
Let's say Carl wants to purchase items from both Daphne and Eve, for 20 Mav each, but only has 30 Mav on his account,
Carl could send the following transactions to the network, with timestamps in this order:
- Transaction A: Carl transfers 20 Mav to Daphne
- Transaction B: Carl transfers 20 Mav to Eve
Now let's say that the following steps happen in this order on a given node:
- Transaction B is received.
- Transaction B is executed. Carl and Eve's balances are updated.
- Eve checks she received payment, and sends her item to Carl
- Transaction A is received.
- The node reverts to the initial state, resetting Carl and Eve's balances.
- Transaction A is executed. Carl and Daphne's balances are updated.
- Transaction B is executed but fails, as Carl doesn't have enough funds on his balance.
- Daphne checks she received payment, and sends her item to Carl
We can see that although Carl only had 30 Mav, he ended up receiving two 20 Mav items, with 10 Mav left on his account.
This situation is a case of what we call double spending: using the same mav for two different transactions. Here, this is done by taking advantage of synchronization issues.
A system where nodes eventually agree with each other on which transactions are executed in which order and on the resulting state is not sufficient: we need a way to eventually reach finality: a situation where a node can guarantee that all the transactions executed up to a point are final, and that the community collectively agrees.
Note that here, Carl could have purposely sent transaction A with a timestamp in the past, after sending transaction B and checking that Eve already sent the item. It could also have simply been due to some congestion issue. As Carl may be in control of some nodes, there is no way to differentiate between the two.
Using blocks
To summarize, we need a way for nodes to collectively agree on which transactions to include, and in which order, with points in time where given transactions become final.
Another way to see it is that we need a mechanism for nodes to collectively agree once and for all on what the next transaction is, and keep doing this indefinitely.
Whatever mechanism we put in place, however, will require nodes to communicate with each other and exchange many messages over the network. Doing all this for every transaction, no matter what the exact mechanism is, would be prohibitively slow and consume a lot of bandwidth.
Assuming we do have a very good mechanism for nodes to agree on the next transaction, how could we significantly reduce the number of messages ?
The solution is to avoid applying this mechanism for every transaction, and instead, group transactions and apply it for every group of transactions. Instead of agreeing on the next transaction, nodes agree in one application of the mechanism, on the next X transactions and their order.
On a blockchain, we call such a group of transactions a block.
A block is mostly a sequence of transactions (and other kinds of operations), to be applied by every node, in order.
Blockchain structure
To summarize what we have seen so far:
- A blockchain is a set of nodes that receive transactions from users, propagate them through a peer-to-peer network, and collectively select them and include them in a sequence of transactions (and other operations).
- For performance reasons, this sequence of transactions is split in groups called blocks, and some mechanism is applied for the nodes to collectively agree on what the next block is, therefore forming a chain of blocks, hence the name blockchain.
- Each node applies every transaction of the chain in the order of the sequence, using the same software, to maintain an internal state. Typically, this state includes the balances of all users' accounts. As all nodes apply the same transactions in the same order, they all end up with the same state.
- Transactions are discarded if they are invalid, either because they are not correctly signed by their author, or when their application is invalid, for example due to lack of funds in the balance of the source account of a transfer.
On top of the sequence of transactions (and other operations), each block contains a header with a small amount of extra information such as a timestamp, the position of the block in the chain (the level), and more, but most importantly, the hash of the content of the previous block. This hash uniquely identifies the previous block, making it a link to this block, forming a chained structure.
As part of the mechanism for nodes to agree on what the next block in the chain should be, multiple blocks may be created and propagated that link to the same previous block. As there can be only one next block in the chain, the goal of the mechanism is to make sure there is consensus on which block should be the official (final) next block.
We will present this in the next module, Consensus Mechanism.