Verax's data model is heavily inspired by Bitcoin's unspent transaction output
(UTXO) model. It is a data where transactions are a set of payments
that are
spent, and a set of new payments
to be created.
Each payment
is a data structure with this information
struct Payment {
created_by_transaction: TxId,
amount: Amount,
destination: Address,
spend_by: Option<TxId>
}
Any payment
that has spend_by: None
is part of the active balance of each
address. Any other payment is not longer part of the active balance, but part of
the historial record. These spent payments are read-only from that point
forward.
Every payment
is created from a transaction, no exception.
flowchart LR
UserA --> |1000 USD| B{Transfer}
B --> |1000 USD| UserB
After this simple transction, UserA
cannot longer spend their 1000 USD
and
balance, and UserB
can spend 1000 USD
more.
This data model does not care how many payments are being spend or created, as long as the amounts are the same on both ends.
In the following example UserA transfer 1000 USD
to UserB
, but 1 USD
is
deducted from the transfer by the system and that is being transfer to the
FeeManager
account.
flowchart LR
UserA --> |1000 USD| B{Transfer}
B --> |999 USD| UserB
B --> |1 USD| FeeManager
As mentioned before, the transaction can spend multiple payments and can create
multiple as well. As long as the amounts are equal on both ends (in this case
1000 USD, 980 EUR
is equal to 999USD + 1 USD, 979EUR + 1EUR
), the
transaction will be valid.
flowchart LR
UserA' --> |1000 USD| B{Transfer}
UserB' --> |980 EUR| B
B --> |999 USD| UserB
B --> |979 EUR| UserA
B --> |1 USD| FeeManager
B --> |1 EUR| FeeManager
When the transaction will be attempted to be persisted, the storage layer will
make sure to flag UserA'
and UserB'
payments. If that operation fails, the
whole transaction creation fails.
Because Verax is heavily inspired in Bitcoin's model, the concurrency model is
quite simple. When a new transaction is commited into the database, each payment
in input
section is attempted to be spent (altering their spend_by
field
from None
to Some(new_transaction_id)
). If any `payment is already spent, or
not valid, the whole transaction creation fails and a rollback is issued upper
stream. The storage layer ensures that transaction creation and updates are
atomic and updates.
sequenceDiagram
Transaction ->>+ DB:s
critical Spend each input
loop Spend Inputs
Transaction ->>+ DB: Spend input
DB ->>+ Transaction: OK
end
DB ->>+ Transaction: OK
loop Output
Transaction ->>+ DB: Creates new output
end
option Success
Transaction ->>+ DB: Commit
DB ->>+ Transaction: OK
option Failure
DB ->>- Transaction: Error
Transaction ->>+ DB: Rollback
end
Because of the input
and output
model, there is no need to check if the
account has enough balance, and there is no need to enforce any locking
mechanism, as long as each selected payment
can be spendable at transaction
storing time, the transaction will be created atomically. Each payment in the
inputs
must spendable, or else the whole operation fails, because every update
is atomic, ensured by the storage layer.
The conditions for a payment ot be spendable are:
payment
is settled. Any other state is not acceptable and will render thisNo global state knowledge is required to be sure that no asset is being created or destroyed by mistake, as long as the inputs are spendable and the sum of amounts inside inputs and outputs matches.