Offchain worker and transactions in polkadot substrate

Explore the basic idea of function overloading based on number of arguments in rust. For this purpose we make use of declarative macros to implement a basic overloaded add function.

DRAFT CONTENT

This is not the final version of this article. I will be completing this once I have some fresh time in my hand

Quick Recap

Although you might already be familier with the terms I am mentioning below, Let’s build a quick recap to bring the context in this article so that anyone with starting or very less knowledge.

Dispatchable Call

In simple terms, dispatchable calls are any function that are exposed for user to interact with. In substrate, they are defined inside impl block prefixed with pallet::call macro. In addition, these are some characteristics of dispatchable call in subtstrate:

  1. They always have weight attached to them

  2. They always have at least one paramater of type OriginFor<T> which defines the origination ( weather it is signed, root or none or even any other custom added origin)

  3. They always return one of DispatchResult or DispatchResultWithPostInfo type

  4. They can read and write to onchain storage and only write to offchain-storage

  5. Dispatchable calls are sent from TransactionPool so it might be possible that some transaction might be filtered beforehand they reach the call ( This is usually done by SignedExtension trait)

Offchain Worker

To put it in easy words, offchain worker are any piece of logic that run seperatly from onchain logic in a way that it neither do nor is able to interfare with block production. In essence, they are kind of seperately running execution isolated from runtime logic but substrate makes it easy by making them easily writeable within the same place of onchain logic. Now again these are quick points about offchain workers:

  1. Their primary function is to discard the load from onchain logic by withdrawing the task that could otherwise require longer than the block execution time.
  2. They can access additional functionality that onchain are restricted to including making http requests, getting node local timestamp and so on.
  3. They have both read and write access to offchain storage
  4. They only have read access to onchain storage. When required write access they have to send the transaction to do so ( that’s why they can be taken as some binary running separately from node itself )
  5. They can be somehow relatable to traditional oracle concept of blockchain but note that these two entities are completely different and serve different use cases
  6. They are defined in Hook trait implying block of pallet which posses [pallet::hooks] macro as well

What’s deal with sending an extrinsic to dispatchable from offchain worker

As I already mentioned, any operation that mutate the state of blockchain have to be done withing onchain runtime logic. This also applies to offchain as even though they are written closely while developing, they run separately from onchain part. Furthermore, it also applies that the behaviour of sending transaction from offchain and sending one from node frontend can be viewed as exactly same thing.

Again, one way to tell weather the source for this transaction is from offchain worker is to https://docs.substrate.io/rustdocs/latest/sp_runtime/transaction_validity/enum.TransactionSource.html

Setting things up

I will assume that you have set you template to use offchain worker and configured keys from which we will send transaction later on. If not, you can create a template from following reference:

Hooks

In general term of programming, hooks are defined as a way to insert extra peice of code after a certain state or point of execution. They are somewhat similar to subscribing for an event and executing the logic after on. There is already been an explanitation on difference & simalirities between hooks and events here:

In context of substrate it is no different. Some example of hooks may be running custom logic when new block is produces, a block is imported and so on.

In fatc, offchain worker is also a hook that is run in seperate context when a block is imported by node. Offchain worker can be configured on when to run during starting the node. This can be done with --offchain-worker parameter. From node-template:

–offchain-worker Should execute offchain workers on every block. By default it’s only enabled for nodes that are authoring new blocks. [default: WhenValidating] [possible values: Always, Never, WhenValidating]

Implementing hooks in pallet

In your pallet, hooks can be implemented by defining an impl block as in:

#[pallet::call]
impl<T: Config> for Self {
    // *--snip
}

#[pallet::hooks]
impl<T: Config> Hooks for Self {
    // Define hooks here
}

For using offchain worker, we have to implement a function named offchain_worker which is actually a hook for offchain-worker as it’s name implies. So we can do something like:

#[pallet::hooks]
impl<T: Config> Hooks for Self {
	fn offchain_worker(_block_number: T::BlockNumber) {
        log::info!("\n\n======> Hello from offchin worker....");
    }
}

Simple right? Make sure you name the function as is. Also we can see that offchain worker recive a paramater of type T::BlockNumber which is actually the block number on which this offchain worker is running. And this function return nothing.

Seeing the offchain worker in action

To see what have we accomplished

cargo run --release -- --dev --tmp --offchain-worker Always

Even if you are running the binary directly by passing --offchain-worker Always this makes sure that offchain worker always runs and we can see the output

While running the node you should see the message we are logging. An output on my machine was:

Defining an example dispatchable call

This must be straight forward just define a exmaple dispatchable call as in:

#[pallet::call]
#[pallet::weight(10_000)]
fn example_call(origin: OriginFor<T>, data: i32) -> DispatchResult {
	log::info!("\n====> An example dispatcable function was called with data: {} ...", data);
    
   if ensure_signed(origin).is_ok() {
       log::info!("Origin is signed..");
    } else if ensure_root(origin).is_ok() {
        log::info!("Origin is root..");
    } else {
        log::info!("Origin is none..");
    }
    
	Ok(())
}

We defined a dispatchable call with weight 10,000 unit and logged a message depending on the origin. This call recived a simple i32 value and simply log it.

Calling the dispatchable from offchain worker

Now at this point, we have a dispatchable to call and offchain worker running. Now before trying to call the dispatchable we just wrote. Here is a thing to catch: Offchain worker always send origin with either signed or none it can never be root. Remember we have mentioned that offchain worker are running seperatly from runtime, and it is guranteed by substrate that root origin can only be produced within the runtime only.

Given that we need an account public address to send signed transaction we have to get one. There are various accounts already preconfigured in substrate template namely Alice, Bob and so on. For the purpose of this demo, we can use any of them.

Inside offchain_worker try to get any of those accounts by:

fn offchain_worker(_block_number: T::BlockNumber) {
    // *---snip
	let signer = Signer::any_account();
}

This will give us one of the account preconfigures in template. Now we can use this to send a signed transaction by using .send_signed_extrinsic method which recived a closure that will return a call to dispatch

let signer = Signer::any_account();
let call_result = signer.send_signed_extrinsic(|account_pubkey| {
	log::info!("Sending a signed transaction from account {}", account_pubkey);
	Call::example_call {
		data: 0,
    }
});

This way we can send a signed transaction to our function example_call we defined earlier. We also log a pubick key of account before sending a transaction.

As of now, we don’t know weather the call was dispatched sucessfully or not. As we are doing nothing with the result call_result in our case. Here, call_result will have type Option<Result<>>. These the possible value of call_result`

  • None

    call_result will be none when there was no account to send transaction from

  • Some(Err(_))

    This specify that the transaction was sent but the transaction itself failed. i.e transaction returned DispatchError or any other error.

  • Some(Ok(_))

    This is returned when the extrinsic was called ans the extrinisc also retrned an Ok()


You have the power

You can Edit this article or even Submit new articles. Want to Learn more?