[−][src]Trait exonum::runtime::Runtime
Runtime environment for Exonum services.
You can read more about the life cycle of services and transactions in the module docs.
Using this trait, you can extend the Exonum blockchain with the services written in different languages.
Stability
This trait is considered unstable; breaking changes may be introduced to it within
semantically non-breaking releases. However, it is guaranteed that such changes
will require reasonable amount of updates from the Runtime
implementations.
Call Ordering
Within the lifetime of a Runtime
, calls to its methods have the following order:
LIFE ::= initialize (GENESIS | RESUME) BLOCK* shutdown
GENESIS ::=
deploy_artifact*
(initiate_adding_service update_service_status)*
after_commit
RESUME ::= (deploy_artifact | update_service_status | migrate)* on_resume
BLOCK* ::= PROPOSAL+ COMMIT
PROPOSAL ::=
(before_transactions CALL*)*
(execute CALL*)*
(after_transactions CALL*)*
CALL ::= execute | initiate_adding_service | initiate_resuming_service | migrate
COMMIT ::=
(deploy_artifact | unload_artifact)*
(update_service_status | migrate)*
after_commit
before_transactions
, execute
and after_transactions
handlers may spawn
child calls among services; this is denoted as CALL*
in the excerpt above. The child calls
are executed synchronously. See the Service Interaction article for more details.
The ordering for the "read-only" methods is_artifact_deployed
and is_supported
in relation
to the lifecycle above is not specified.
Consensus and Local Methods
The following methods should return the same result if provided arguments are the same for all the nodes in the blockchain network:
before_transactions
execute
after_transactions
initiate_adding_service
initiate_resuming_service
All these methods should also produce the same changes to the storage via
the provided ExecutionContext
. Discrepancy in node behavior within these methods may lead
to a consensus failure.
Other Runtime
methods may execute logic specific to the node.
Handling Panics
Panics in the Runtime
methods are not caught. A panic in the runtime method will cause
the node termination. To catch panics in the Rust code and convert them to unchecked execution
errors, use the catch_panic
method.
Required methods
fn deploy_artifact(
&mut self,
artifact: ArtifactId,
deploy_spec: Vec<u8>
) -> Receiver
&mut self,
artifact: ArtifactId,
deploy_spec: Vec<u8>
) -> Receiver
Requests to deploy an artifact with the given identifier and an additional deploy specification.
This method is called once for a specific artifact during the Runtime
lifetime:
- For newly added artifacts, the method is called as the supervisor service decides to deploy the artifact.
- After the node restart, the method is called for all the previously deployed artifacts.
Core guarantees that there will be no request to deploy an artifact which is already deployed,
thus runtime should not report an attempt to do so as ExecutionError
, but should consider it
a bug in core.
fn is_artifact_deployed(&self, artifact: &ArtifactId) -> bool
Returns true
if the specified artifact is deployed in this runtime.
fn initiate_adding_service(
&self,
context: ExecutionContext,
artifact: &ArtifactId,
parameters: Vec<u8>
) -> Result<(), ExecutionError>
&self,
context: ExecutionContext,
artifact: &ArtifactId,
parameters: Vec<u8>
) -> Result<(), ExecutionError>
Runs the constructor of a new service instance with the given specification and initial arguments. The constructor can initialize the storage of the service, check for dependencies, etc.
The constructor runs exactly once during the blockchain lifetime for each successfully initialized service instance. That is to say, the constructor is not called on a node restart.
At the same time, when initiate_adding_service
is called,
there is no guarantee that the service will eventually get to the blockchain via
update_service_status
. The consensus may accept an alternative block proposal, in which
the service is not instantiated or instantiated with different parameters.
The update_service_status
call always takes place
in the closest committed block, i.e., before the nearest Runtime::after_commit()
.
The dispatcher routes transactions and before_transactions
/ after_transactions
events to the service only after update_service_status()
is called with the same instance
specification.
The runtime should discard the instantiated service instance after completing this method.
Otherwise, if the service is successfully committed in the block, it will duplicate the one
instantiated in the runtime. There may be compelling reasons for the runtime to retain
the instantiated service. For example, if creating an instance takes very long time.
In this case, the "garbage" services may be removed from the runtime in after_commit
because of the time dependence between update_service_status
and after_commit
described above.
The runtime should commit long-term resources for the service only after the
update_service_status()
call. In other words, the runtime must be sure that the service
has been committed to the blockchain.
Return Value
Returning an error is a signal of Runtime
that the
service instantiation has failed. As a rule of a thumb, changes made by the
initiate_adding_service
method will be rolled back after such a signal. The exact logic of
the rollback is determined by the supervisor.
An error is one of the expected / handled outcomes of the service instantiation procedure.
Thus, verifying prerequisites
for instantiation and reporting corresponding failures should be performed at this stage
rather than in update_service_status
.
Core guarantees that there will be no request to start a service instance which is already running,
thus runtime should not report an attempt to do so as ExecutionError
, but should consider it
a bug in core.
fn initiate_resuming_service(
&self,
context: ExecutionContext,
artifact: &ArtifactId,
parameters: Vec<u8>
) -> Result<(), ExecutionError>
&self,
context: ExecutionContext,
artifact: &ArtifactId,
parameters: Vec<u8>
) -> Result<(), ExecutionError>
Resumes previously stopped service instance with the given specification and arguments. As an example, arguments can be used to update the service configuration.
The dispatcher ensures that a service instance with the given specification has been previously stopped and has the proper artifact version and name.
This method has the same workflow as initiate_adding_service
method.
The main difference is that initiate_adding_service
should call the service
initialize
method and initiate_resuming_service
should call the service resume
method.
fn update_service_status(
&mut self,
snapshot: &dyn Snapshot,
state: &InstanceState
)
&mut self,
snapshot: &dyn Snapshot,
state: &InstanceState
)
Notifies runtime about changes of the service instance state.
This method notifies runtime about a specific service instance state changes in the dispatcher. Runtime should perform corresponding actions in according to changes in the service instance state.
This method is called for a specific service instance during the Runtime
lifetime in the
following cases:
- For newly added instances, or modified existing this method is called when the fork with the corresponding changes is committed.
- After a node restart, the method is called for all existing service instances regardless of their statuses.
For newly added instances invocation of this method guarantees that
initiate_adding_service()
has been called with the same spec
already and returned
Ok(())
. The results of the call (i.e., changes to the blockchain state) will be
persisted from the call.
Arguments
snapshot
is a storage snapshot at the latest height when the method is called:
- Suppose the service is committed during the node operation. Then
snapshot
is taken at the moment the fork applies for which the correspondinginitiate_adding_service
has been performed. - Suppose the service is stopped during the node operation.
Then
snapshot` is taken at the moment the fork applies for which the corresponding request has been performed. - Suppose the service resumes after the node restart. Then
snapshot
is the storage state at the node start.
For the built-in services, on the first node start snapshot
will not contain information
on the genesis block. Thus, using some core APIs, like requesting the current
blockchain height, will result in a panic.
status
is the resulting status of the service instance.
Return value
This method does not return a value, meaning that any error occurred during this method execution is considered critical and should lead to the node stopping.
It is assumed that if initiate_adding_service
didn't return an error previously,
the runtime is able to update service status and within normal conditions no error is
expected to happen.
fn migrate(
&self,
new_artifact: &ArtifactId,
data_version: &Version
) -> Result<Option<MigrationScript>, InitMigrationError>
&self,
new_artifact: &ArtifactId,
data_version: &Version
) -> Result<Option<MigrationScript>, InitMigrationError>
Gets the migration script to migrate the data of the service to the state usable by a newer version of the artifact.
An implementation of this method should be idempotent, i.e., return the same script or error for the same input.
Invariants Ensured by the Caller
new_artifact
is deployed in the runtimedata_version < new_artifact.version
Return Value
- An error signals that the runtime does not know how to migrate the service to a newer version.
Ok(Some(_))
provides a script to execute against service data. After the script is executed,data_version
of the service will be updated toend_version
from the script.end_version
does not need to correspond to the version ofnew_artifact
, or to a version of an artifact deployed on the blockchain in general.Ok(None)
means that the service does not require data migration.data_version
of the service will be updated to the version ofnew_artifact
once the block with the migration command is committed; see Service State Transitions for details.
fn execute(
&self,
context: ExecutionContext,
method_id: MethodId,
arguments: &[u8]
) -> Result<(), ExecutionError>
&self,
context: ExecutionContext,
method_id: MethodId,
arguments: &[u8]
) -> Result<(), ExecutionError>
Dispatches payload to the method of a specific service instance.
The call is dispatched iff the service is considered active at the moment. See Service State Transitions for more details.
Arguments
The service instance name and method ID are provided in the call_info
argument and
the interface name is provided as the corresponding field of the context
argument.
A blank interface name denotes the "default" interface; it should be supported by all services. The methods of the default interface are defined by the service artifact and thus may have different signatures for different services.
A non-empty interface name denotes an interface defined externally to the service instance.
In this case, the name is a Protobuf flavor of a fully qualified name
(e.g., exonum.Configure
). And the method signatures can be inferred from the name
using an interface definition.
Note. Support of non-default interfaces is experimental; as such, an IDL for them is not stabilized yet.
Return Value
- If the service does not implement an interface, returns a
NoSuchInterface
error. - If the interface does not have a method, returns a
NoSuchMethod
error.
An error returned from this method will lead to the rollback of all changes
in the fork enclosed in the context
.
fn before_transactions(
&self,
context: ExecutionContext
) -> Result<(), ExecutionError>
&self,
context: ExecutionContext
) -> Result<(), ExecutionError>
Notifies a service stored in the present runtime about the beginning of the block. Allows the service to modify the blockchain state before any transaction in the block is processed.
before_transactions
is called for every service active at the beginning of the block
exactly once for each block. Services that will be instantiated within the block do not
receive a call. The method is not called for the genesis block.
Return Value
An error returned from this method will lead to the rollback of all changes
in the fork enclosed in the context
.
fn after_transactions(
&self,
context: ExecutionContext
) -> Result<(), ExecutionError>
&self,
context: ExecutionContext
) -> Result<(), ExecutionError>
Notifies a service stored in this runtime about the end of the block. Allows the method to modify the blockchain state after all transactions in the block are processed.
after_transactions
is called for every service active at the beginning of the block
exactly once for each block. Services instantiated within the block do not receive a call.
Services instantiated within genesis block are activated immediately and
thus after_transactions
is invoked for them in the genesis block.
Return value
An error returned from this method will lead to the rollback of all changes
in the fork enclosed in the context
.
fn after_commit(&mut self, snapshot: &dyn Snapshot, mailbox: &mut Mailbox)
Notifies the runtime about commit of a new block.
This method is called after all update_service_status
calls related
to the same block. The method is called exactly once for each block in the blockchain,
including the genesis block.
A block is not yet persisted when this method is called. The snapshot
provides an up-to-date
block information. It corresponds exactly to the information
eventually persisted.
mailbox
is used to send async commands to the dispatcher. This mechanism is used, e.g.,
by the supervisor service to enqueue artifact deployment. A runtime may ignore mailbox
if its services (or the runtime itself) do not require privileged access to the dispatcher.
Provided methods
fn initialize(&mut self, blockchain: &Blockchain)
Initializes the runtime, providing a Blockchain
instance for further use.
Calling this method always takes place before calling any other Runtime
methods.
The initialize
method is called exactly once during the Runtime
lifetime.
The default implementation does nothing.
fn is_supported(&self, feature: &RuntimeFeature) -> bool
Checks if the runtime supports an optional feature.
This method can be called by the core before performing operations that might not be implemented in a runtime, or by the supervisor service in order to check that a potential service / artifact state transition can be handled by the runtime.
An implementation should return false
for all features the runtime does not recognize.
The default implementation always returns false
, i.e., signals that the runtime supports
no optional features.
fn on_resume(&mut self)
Notifies the runtime that the dispatcher has completed re-initialization after the node restart. Re-initialization includes restoring the deployed artifacts / started service instances for all the runtimes.
This method is called maximum once during the Runtime
lifetime. It is called iff
the genesis block was already created before the node start (e.g. after node relaunch).
The blockchain state will remain the same between the initialize
and on_resume
calls.
The default implementation does nothing.
fn unload_artifact(&mut self, artifact: &ArtifactId)
Requests to unload an artifact with the given identifier. Unloading may free resources (e.g., RAM) associated with the artifact.
The following invariants are guaranteed to hold when this call is performed:
- The artifact is deployed
- There are no services with any status associated with the artifact, either as an artifact responsible for service logic or as a migration target of the data migration in a service.
The default implementation does nothing. While this may be inefficient, this implementation is logically sound. Indeed, the runtime retains resources associated with the artifact (until the node is restarted), but on the blockchain level, the artifact is considered unloaded.