Unlike some other blockchain frameworks, Exonum is built with the first-class support of business logic evolution. Service artifacts may evolve during blockchain operation, which is reflected in the fact that a semantic version is a built-in part of the artifact. This article dives into details how versioning should be implemented and how it is used in the framework.
The problem solved by versioning is as follows. Exonum services have clients, both internal (other services on the same blockchain) and external (e.g., light clients and other software capable of submitting transactions). For a multitude of reasons, the clients may have different idea as to the service capabilities than the reality at hand.
Here's hypothetical manifestations of the problem:
- The client thinks service with a certain ID is a crypto-token service, but in reality it is a time oracle.
- The client correctly thinks that a service with a certain ID is a crypto-token service, but is unaware that the format of the transfer transaction has changed.
- The client (another service) attempts to get the consolidated time from the schema of a time oracle, but in reality it's not a time oracle. (Or it is a newer time oracle with changed schema layout.)
In all these cases, the lack of knowledge on the client side may lead to unpredictable consequences. In the best case, a transaction constructed by such a client will turn out to be garbage from the service perspective, so it will just return a deserialization error. In the worst case, the transaction may be interpreted arbitrarily. The same reasoning is true for the service schema; in the best case, accessing the bogus schema will lead to an error due to the mismatch of expected an actual index types. In the worst case, the indexes will be accessed, but will return garbage data or lead to undefined behavior of the node.
Solution: Semantic Versioning¶
For any reasonable solution to the problem above to work, Exonum artifacts must be semantically versioned. Indeed, semantic versioning allows to reason about client / service compatibility in terms other than “Any specific version of a service artifact is absolutely incompatible with any other version.”
Correct versioning is the responsibility of the service developers; the framework does not (and cannot) check versioning automatically.
The general guidelines to maximize service longevity are:
- Versioning concerns all public interfaces of the service. As of Exonum 1.0, these interfaces are transactions and the (public part of) service schema.
- Transaction methods can be evolved much like Protobuf messages (in fact, transaction payloads should be Protobuf messages for this reason). Semantics of a method with the given ID must never change; in particular, the method ID must never be reused.
- Removing a method or disabling processing for certain payloads should be considered a breaking change (with a possible exclusion of bug fixes).
- Public service schema should expose the minimum possible number of indexes, since the changes in these indexes will be breaking.
To be able to process transactions, service must have a static mapping between numeric identifier of a transaction and the transaction handler. Logic of transaction processing may include deserializing input parameters from a byte array, processing the input and reporting the execution result (which can be either successful or unsuccessful).
Numeric identifier of a transaction (i.e.,
method_id within the
is considered a constant during all the time of service existence. If a transaction
was declared with certain ID, its logic can be updated (e.g., to fix a bug)
or be removed, but it never should be replaced with another transaction.
Although HTTP API is runtime-specific interface, the best practices regarding API compatibility still apply:
- HTTP endpoints may be prefixed with a version, e.g.,
- Removed endpoints should return
- Deprecated endpoints can emit a deprecation warning, e.g., via
a specialized header or
- Endpoints may employ redirection from an older endpoint to
a newer one with a
301 Moved Permanentlystatus
Versioning in Core¶
Artifact versions are used in the core, in particular, in data migrations. Indeed, a service can only be migrated from one artifact to another if the target artifact is a newer version of the source artifact.
A service with artifact
some.Token@0.2.0 can be migrated
some.Token@0.4.1, but not to
Versioning for Clients¶
The client may check the name and version of the artifact for a specific service using builtin APIs provided by the Exonum core. For example, the core maintains a list of deployed artifacts and instantiated services, which can be retrieved from a schema (for services) or system API (for external clients).
For transactions, clients may use the middleware service in order to
make checked calls to the service.
A checked call is executed only if the targeted service has an expected
artifact name and version requirement (for example,
For schemas, Rust code may use safe data access provided
BlockchainData types. This access checks
artifact name / version requirement under the hood.