A transaction in Exonum, as in usual databases, is a group of sequential operations with the data (i.e., the Exonum key-value storage). Transaction processing rules are defined in services; these rules determine business logic of any Exonum-powered blockchain.
Transactions are executed atomically, consistently, in isolation and durably. If the transaction execution violates certain data invariants, the transaction is completely rolled back, so that it does not have any effect on the persistent storage.
If the transaction is correct, it can be committed, i.e., included into a block via the consensus algorithm among the blockchain validators. Consensus provides total ordering among all transactions; between any two transactions in the blockchain, it is possible to determine which one comes first. Transactions are applied to the Exonum key-value storage sequentially in the same order transactions are placed into the blockchain.
The order of transaction issuance at the client side does not necessarily correspond to the order of their processing. To maintain the logical order of processing, it is useful to adhere to the following pattern: send the next transaction only after the previous one was processed. This behavior is already implemented in the light client library.
All transactions are authenticated with the help of public-key digital signatures. Generally, a transaction contains the signature verification key (aka public key) among its parameters. Thus, authorization (verifying whether the transaction author actually has the right to perform the transaction) can be accomplished with the help of building a public key infrastructure and/or various constraints based on this key.
It is recommended for transaction signing to be decentralized in order to minimize security risks. Roughly speaking, there should not be a single server signing all transactions in the system; this could create a security chokepoint. One of options to decentralize signing is to use the light client library.
In a sample cryptocurrency service, an owner of cryptocurrency may authorize transferring his coins by signing a transfer transaction with a key associated with coins. Authentication in this case means verifying that a transaction is digitally signed with a specific key, and authorization means that this key is associated with a sufficient amount of coins to make the transaction.
All transactions in Exonum are templated. Every Exonum transaction is defined by its template and a set of parameters, rather than by an overt sequence of operations on the key-value storage. The sequence of operations can be unambiguously restored given a template identifier and template parameters. This design leads to a more safe and controlled environment for transactional processing.
Transaction templates are defined in services and could be viewed as an analogue to stored procedures in database management systems, or to POST/PUT endpoints in web services. Similar to these cases, the goal of templating is to restrict eligible transaction patterns (e.g., to preserve certain invariants) and to separate implementation details from transaction invocation.
From the computer science perspective, an arbitrary Exonum transaction
can be defined as
Tx: S -> S, where
S denotes the key-value storage type.
Templating corresponds to defining parameterized families of transactions
TxTemplate(i: I): P(i) -> S -> S,
I is the set of defined transaction families and
is the parameter space for the
ith family. Correspondingly, any transaction
in Exonum is a partially applied function
with the transaction family and parameters fixed.
Transactions in Exonum are subtypes of messages and share the serialization logic with consensus messages (see the Serialization article for more details). All transaction messages are serialized in a uniform fashion. There are 2 serialization formats:
- Binary serialization is used in communication among nodes and to persist transactions in the storage
- JSON is used to receive and send transactions when communicating with light clients
Although light clients communicate with full nodes using the JSON format, they implement serialization internally in order to sign transactions and calculate their hashes.
Each unique transaction message serialization is hashed with SHA-256 hash function. A transaction hash is taken over the entire transaction serialization (including its signature). Hashes are used as unique identifiers for transactions where such an identifier is needed (e.g., when determining whether a specific transaction has been committed previously).
Transaction body includes data specific for a given
transaction type. Format of the body is specified by the
service identified by
Binary serialization of the body is performed using
the Exonum serialization format
according to the transaction specification in the service.
The body of
TxTransfer transaction type in the sample cryptocurrency service
is structured as follows:
|Field||Binary format||Binary offset||JSON|
from is the coins sender,
to is the coins recipient,
amount is the amount being transferred, and
seed is a randomly generated field to distinct among transactions
with the same previous three fields.
u64 values are serialized as strings in JSON, as they may be
Binary presentation: binary sequence with
JSON presentation: JSON.
fn verify(&self) -> bool;
verify method verifies the transaction, which includes the message
signature verification and other specific internal constraints.
verify is intended to check the internal consistency of a transaction;
it has no access to the blockchain state.
If a transaction fails
verify, it is considered incorrect and cannot
be included into any correct block proposal. Incorrect transactions are never
included into the blockchain.
In the cryptocurrency service,
TxTransfer.verify checks the digital signature and ensures that
the sender of coins is not the same as the receiver.
fn execute(&self, view: &mut Fork) -> ExecutionResult;
execute method takes the current blockchain state and can modify it (but can
choose not to if certain conditions are not met). Technically
operates on a fork of the blockchain state, which is merged to the persistent
storage under certain conditions.
execute are triggered at different times.
internal consistency of a transaction before the transaction is included
into a pool of unconfirmed transactions.
execute is performed during the
Precommit stage of consensus and when
the block with the given transaction is committed into the blockchain.
In the sample cryptocurrency service,
that the sender’s and recipient’s accounts exist and the sender has enough
coins to complete the transfer. If these conditions hold, the sender’s
balance of coins is decreased and the recipient’s one is increased by the amount
specified in the transaction. Additionally, the transaction is logged in the
sender’s and recipient’s history of transactions; the logging is performed even
if the transaction execution is unsuccessful (e.g., the sender has insufficient
number of coins). Logging helps to ensure that
the account state is verifiable by light clients.
execute method can signal that a transaction should be aborted
by returning an error. The error contains a transaction-specific error code
(an unsigned 1-byte integer), and an optional string description. If
returns an error, all changes made in the blockchain state by the transaction
are discarded; instead, the error code and description are saved to the blockchain.
execute method of a transaction raises an unhandled exception (panics
in the Rust terms), the changes made by the transactions are similarly discarded.
Erroneous and panicking transactions are still considered committed. Such transactions can be and are included into the blockchain provided they lead to the same result (panic or return an identical error code) for at least 2/3 of the validators.
A transaction is created by an external entity (e.g., a light client) and is signed with a private key necessary to authorize the transaction.
2. Submission to Network
After creation, the transaction is submitted to the blockchain network. Usually, this is performed by a light client connecting to a full node via an appropriate transaction endpoint.
As transactions use universally verified cryptography (digital signatures) for authentication, a transaction theoretically can be submitted to the network by anyone aware of the transaction. There is no intrinsic upper bound on the transaction lifetime, either.
From the point of view of a light client, transaction execution is
asynchronous; full nodes do not return an execution status synchronously
in a response to a client’s request. To determine transaction status,
you may poll the transaction status using read requests
defined in the corresponding service or the blockchain explorer.
If a transaction is valid (i.e., its
true), it’s expected
to be committed in a matter of seconds.
After a transaction is received by a full node, it is looked up among committed transactions, using the transaction hash as the unique identifier. If a transaction has been committed previously, it is discarded, and the following steps are skipped.
The transaction implementation is then looked up
(service_id, message_id) type identifier.
verify method of the implementation is invoked to check the internal
consistency of the transaction.
If the verification is successful, the transaction is added to the pool
of unconfirmed transactions; otherwise, it is discarded, and the following
steps are skipped.
If a transaction included to the pool of unconfirmed transactions is received by a node not from another full node, then the transaction is broadcast to all full nodes that the node is connected to. In particular, a node broadcasts transactions received from light clients or generated internally by services, but does not rebroadcast transactions that are broadcast by peer nodes or are received with the help of requests during consensus.
After a transaction reaches the pool of a validator, it can be included into a block proposal (or multiple proposals).
Presently, the order of inclusion of transactions into a proposal is determined by the transaction hash. An honest validator takes transactions with the smallest hashes when building a proposal. This behavior shouldn’t be relied upon; it is likely to change in the future.
executes during the
lock step of the consensus algorithm. This happens when a validator
has collected all
transactions for a block proposal and certain conditions are met, which imply
that the proposal is going to be accepted in the near future.
The results of execution are reflected in
Precommit consensus messages and
are agreed upon within the consensus algorithm. This allows to ensure that transactions
are executed in the same way on all nodes.
When a certain block proposal and the result of its execution gather
sufficient approval among validators, a block with the transaction is committed
to the blockchain. All transactions from the committed block are sequentially applied
to the persistent blockchain state by invoking their
in the same order the transactions appear in the block.
Hence, the order of application is the same for every node in the network.
verify in transactions is pure,
which means that the verification result doesn’t depend on the
blockchain state and the local environment of the verifier. Thus, transaction
verification could easily be
parallelized over transactions. Moreover, it’s sufficient to verify any transaction
only once – when it’s submitted to the pool of unconfirmed transactions.
As a downside,
verify cannot perform any checks that depend on the blockchain
state. For example, in the cryptocurrency service,
cannot check whether the sender has sufficient amount of coins to transfer.
Sequential consistency essentially means that the blockchain looks like a centralized system for an external observer (e.g., a light client). All transactions in the blockchain affect the blockchain state as if they were executed one by one in the order specified by their ordering in blocks. Sequential consistency is guaranteed by the consensus algorithm.
Non-replayability means that an attacker cannot take an old legitimate transaction from the blockchain and apply it to the blockchain state again.
Assume Alice pays Bob 10 coins using the sample cryptocurrency service. Non-replayability prevents Bob from taking Alice’s transaction and submitting it to the network again to get extra coins.
Non-replayability is also a measure against DoS attacks; it prevents an attacker from spamming the network with his own or others’ transactions.
Non-replayability in Exonum is guaranteed by discarding transactions already included into the blockchain (which is determined by the transaction hash), on the verify step.
If a transaction is not idempotent, it needs to have
an additional field to distinguish among transactions with the same
set of parameters. This field needs to have a sufficient length (e.g., 8 bytes)
and can be generated deterministically (e.g., via a counter) or
TxTransfer.seed in the cryptocurrency service
as an example.