[][src]Trait exonum::runtime::Runtime

pub trait Runtime: Send + Debug + 'static {
    fn deploy_artifact(
        &mut self,
        artifact: ArtifactId,
        deploy_spec: Vec<u8>
    ) -> Receiver;
fn is_artifact_deployed(&self, artifact: &ArtifactId) -> bool;
fn initiate_adding_service(
        &self,
        context: ExecutionContext,
        artifact: &ArtifactId,
        parameters: Vec<u8>
    ) -> Result<(), ExecutionError>;
fn initiate_resuming_service(
        &self,
        context: ExecutionContext,
        artifact: &ArtifactId,
        parameters: Vec<u8>
    ) -> Result<(), ExecutionError>;
fn update_service_status(
        &mut self,
        snapshot: &dyn Snapshot,
        state: &InstanceState
    );
fn migrate(
        &self,
        new_artifact: &ArtifactId,
        data_version: &Version
    ) -> Result<Option<MigrationScript>, InitMigrationError>;
fn execute(
        &self,
        context: ExecutionContext,
        method_id: MethodId,
        arguments: &[u8]
    ) -> Result<(), ExecutionError>;
fn before_transactions(
        &self,
        context: ExecutionContext
    ) -> Result<(), ExecutionError>;
fn after_transactions(
        &self,
        context: ExecutionContext
    ) -> Result<(), ExecutionError>;
fn after_commit(&mut self, snapshot: &dyn Snapshot, mailbox: &mut Mailbox); fn initialize(&mut self, blockchain: &Blockchain) { ... }
fn is_supported(&self, feature: &RuntimeFeature) -> bool { ... }
fn on_resume(&mut self) { ... }
fn unload_artifact(&mut self, artifact: &ArtifactId) { ... } }

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

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>

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>

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
)

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 corresponding initiate_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>

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 runtime
  • data_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 to end_version from the script. end_version does not need to correspond to the version of new_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 of new_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>

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>

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>

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.

Loading content...

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 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.

Loading content...

Implementors

Loading content...