It is often necessary we fetch external data in our blockchain applications. For traditional back-end applications this can be done with simple HTTP REST-ful fetch requests, but not the case in blockchain applications. Firstly it is usually not necessary for every nodes in the network to fetch from the remote end to verify the data. Secondly the http request does not return a deterministic result as there maybe network delays or even errors. This potentially cause block production delays and problems and affect the stability of the blockchain application.
What we need is some kind of a worker that is outside the regular state transition cycle during block production, an off-chain worker, to execute these operations by a few dedicated nodes in the network.
Substrate, a flexible blockchain development framework does offer this feature of Off-chain Worker. This project is a demonstration of this feature.
This project consists of a Substrate node with a custom pallet that use off-chain worker to fetch prices of a few cryptocurrencies, each from two external sources and then aggregate them by simply averaging and recording them back to on-chain storage.
This project has a setup of using
Substrate Node Template
in the node
folder, and
Substrate Front-end Template
in the frontend
folder.
The meat of the project is in substrate/node/runtime/src/lib.rs
and
substrate/pallets/price-fetch/src/lib.rs
.
In this file, in addition to the regular lib setup, we specify a SubmitPFTransaction
type and set
it as the associated type for our pallet trait. We then implement the
system::offchain::CreateTransaction
trait afterwards as follows. This is necessary for the
off-chain worker to send signed or unsigned transactions back on-chain.
// -- snip --
type SubmitPFTransaction = system::offchain::TransactionSubmitter<
price_fetch::crypto::Public,
Runtime,
UncheckedExtrinsic
>;
parameter_types! {
pub const BlockFetchDur: BlockNumber = 2;
}
impl price_fetch::Trait for Runtime {
type Event = Event;
type Call = Call;
type SubmitSignedTransaction = SubmitPFTransaction;
type SignAndSubmitTransaction = SubmitPFTransaction;
type SubmitUnsignedTransaction = SubmitPFTransaction;
type BlockFetchDur = BlockFetchDur;
}
impl system::offchain::CreateTransaction<Runtime, UncheckedExtrinsic> for Runtime {
type Public = <Signature as Verify>::Signer;
type Signature = Signature;
fn create_transaction<TSigner: system::offchain::Signer<Self::Public, Self::Signature>> (
call: Call,
public: Self::Public,
account: AccountId,
index: Index,
) -> Option<(Call, <UncheckedExtrinsic as sp_runtime::traits::Extrinsic>::SignaturePayload)> {
// ...
}
}
// -- snip --
Feel free to look at the src code side by side with the following description.
Note the necessary included modules at the top for:
system::{ offchain, ...}
, off-chain workers, for submitting transactionsimple_json
, a lightweight json-parsing library that work inno_std
environmentsp_runtime::{...}
, some libraries to handle HTTP requests and sending transactions back on-chain.
The main logic of this pallet goes as:
-
After a block is produced,
offchain_worker
function is called. This is the main entry point of the Substrate off-chain worker. It checks that for everyBlockFetchDur
blocks (set inlib.rs
), the off-chain worker goes out and fetch the price based on the config data inFETCHED_CRYPTOS
. -
For each entry of the
FETCHED_CRYPTOS
, it executes thefetch_price
function, and get the JSON response back fromfetch_json
function. The JSON is parsed for a specific format to return the price in (USD dollar * 1000) in integer format. -
We then submit an unsigned transaction to call the on-chain extrinsics
record_price
function to record the price. All unsigned transactions by default are regarded as invalid transactions, so we explicitly enable them in thevalidate_unsigned
function. -
On the next block production, the price is stored on-chain. The
SrcPricePoints
stores the actual price + timestamp (price point), andTokenSrcPPMap
andRemoteSrcPPMap
are the indices mapping the cryptocurrency and remote src to some price points. FinallyUpdateAggPP
are incremented by one so later on we know how many new price points have arrived since we last calculated the mean of the crypto price. -
Once a block is produced,
offchain_worker
function kicks in again, and see thatUpdateAggPP
have mapping value(s) greater than 0, then theaggregate_pp
function is called. This function retrieves the lastUpdateAggPP
mapping value, being passed in to the function asfreq
parameter, of that crypto prices and find the mean of each crypto price. Then we submit an unsigned transaction to call the on-chain extrinsicsrecord_agg_pp
function to record the mean price of the cryptocurrencies, with logic similar to therecord_price
function.
It is based on Substrate Front-end Template with a new section to show cryptocurrency prices.
-
For the node, same instructions as Substrate Node Template
-
For front end, same instructions as Substrate Front-end Template
- Tracked in issue #11.