Writing the Contract Binary

The contract binary is the first component of a Linera application. It can actually change the state of the application.

To create a contract, we need to create a new type and implement the Contract trait for it, which is as follows:

#[async_trait]
pub trait Contract: WithContractAbi + ContractAbi + Send + Sized {
    /// The type used to report errors to the execution environment.
    type Error: Error + From<serde_json::Error> + From<bcs::Error> + 'static;

    /// The type used to store the persisted application state.
    type State: Sync;

    /// The desired storage backend used to store the application's state.
    type Storage: ContractStateStorage<Self> + Send + 'static;

    /// The type of message executed by the application.
    type Message: Serialize + DeserializeOwned + Send + Sync + Debug + 'static;

    /// Creates an in-memory instance of the contract handler from the application's `state`.
    async fn new(state: Self::State, runtime: ContractRuntime<Self>) -> Result<Self, Self::Error>;

    /// Returns the current state of the application so that it can be persisted.
    fn state_mut(&mut self) -> &mut Self::State;

    /// Initializes the application on the chain that created it.
    async fn initialize(
        &mut self,
        argument: Self::InitializationArgument,
    ) -> Result<(), Self::Error>;

    /// Applies an operation from the current block.
    async fn execute_operation(
        &mut self,
        operation: Self::Operation,
    ) -> Result<Self::Response, Self::Error>;

    /// Applies a message originating from a cross-chain message.
    async fn execute_message(&mut self, message: Self::Message) -> Result<(), Self::Error>;

    /// Finishes the execution of the current transaction.
    async fn finalize(&mut self) -> Result<(), Self::Error> {
        Self::Storage::store(self.state_mut()).await;
        Ok(())
    }
}

The full trait definition can be found here.

There's quite a bit going on here, so let's break it down and take one method at a time.

For this application, we'll be using the initialize and execute_operation methods.

The Contract Lifecycle

To implement the application contract, we first create a type for the contract:

pub struct CounterContract {
    state: Counter,
    runtime: ContractRuntime<Self>,
}

This type usually contains at least two fields: the persistent state defined earlier and a handle to the runtime. The runtime provides access to information about the current execution and also allows sending messages, among other things. Other fields can be added, and they can be used to store volatile data that only exists while the current transaction is being executed, and discarded afterwards.

When a transaction is executed, first the application's state is loaded, then the contract type is created by calling the Contract::new method. This method receives the state and a handle to the runtime that the contract can use. For our implementation, we will just store the received parameters:

    async fn new(state: Counter, runtime: ContractRuntime<Self>) -> Result<Self, Self::Error> {
        CounterContract { state, runtime }
    }

When the transaction finishes executing successfully, there's a final step where all loaded application contracts are allowed to finalize, similarly to executing a destructor. The default implementation of finalize just persists the application's state, and that's why we must provide it access to the state through the state_mut method:

    fn state_mut(&mut self) -> &mut Self::State {
        &mut self.state
    }

Applications may want to override the finalize method in more advanced scenarios, but they must ensure they don't forget to persist their state if they do so. For more information see the Contract finalization section.

Initializing our Application

The first thing that happens when an application is created from a bytecode is that it is initialized. This is done by calling the contract's Contract::initialize method.

Contract::initialize is only called once when the application is created and only on the microchain that created the application.

Deployment on other microchains will use the Default implementation of the application state if SimpleStateStorage is used, or the Default value of all sub-views in the state if the ViewStateStorage is used.

For our example application, we'll want to initialize the state of the application to an arbitrary number that can be specified on application creation using its initialization parameters:

    async fn initialize(&mut self, value: u64) -> Result<(), Self::Error> {
        self.state.value.set(value);
        Ok(())
    }

Implementing the Increment Operation

Now that we have our counter's state and a way to initialize it to any value we would like, we need a way to increment our counter's value. Execution requests from block proposers or other applications are broadly called 'operations'.

To create a new operation, we need to use the method Contract::execute_operation. In the counter's case, it will be receiving a u64 which is used to increment the counter:

    async fn execute_operation(&mut self, operation: u64) -> Result<(), Self::Error> {
        let current = self.value.get();
        self.value.set(current + operation);
        Ok(())
    }

Declaring the ABI

Finally, to link our Contract trait implementation with the ABI of the application, the following code is added:

impl WithContractAbi for CounterContract {
    type Abi = counter::CounterAbi;
}