2023.01.12

Substrate理解の第一歩:palletでブロックチェーンへ機能追加

はじめに

Y.C.です。ブロックチェーン構築フレームワークであるSubstrateについて理解を進めています。

前回の記事では、Substrateブロックチェーンにおけるコントラクトについて調べてみました。その中で、コントラクト実行環境がpalletとして提供されていることがわかりました。

今回はそのpalletがどう作られどう機能するのかを明らかにし、Substrateがブロックチェーン構築フレームワークたる所以に踏み込んでいきたいと思います。

TL;DR

  • palletはsubstrateブロックチェーンに機能を追加するモジュールのようなもの
  • palletを組み合わせることでブロックチェーンを構築していく
  • hookを利用することでEthereumでは不可能だった定期実行のような高度な処理が実現可能

palletとは

palletは、Substrateブロックチェーンに機能を提供するモジュールです。ユースケースに応じて様々なpalletをruntimeに追加することで、独自のブロックチェーンを構築することができます。別のpalletの機能を組み合わせより高機能なpalletを実現することもできます。

 

Compose a runtime with FRAME
https://docs.substrate.io/fundamentals/runtime-development/

palletの例

 

pallet_balances

accountとbalanceを取り扱う機能を提供します。ブロックチェーンの根幹を成す機能ですが、システムライブラリではなくpalletという形なのが意外です。独自のbalanceの仕組みを実装した場合に、pallet_balancesを置き換えるだけで簡単に組み込むことができる、というのがメリットでしょうか。

pallet_timestamp

現在のブロックのtimestampを取得する機能を提供します。後述するinherentで実現しています。

pallet_scheduler

特定のblock number、 あるいは期間を指定することで、なんと処理のスケジューリングが可能になります!!! 後述するhookで実現しています。

pallet_contracts

WebAssemblyスマートコントラクトの実行機能を提供します。

pallet_evm

EVMスマートコントラクトの実行機能を提供します。

 

最小構成pallet-template

公式チュートリアルでは、pallet-nicksというpalletを導入します。これはアカウントIDにニックネームを登録することができるという単純な機能を提供するものです。しかしコードを読むとアカウントの存在確認をしていたりと、意外と複雑なことをしています。

個人的には、新しいことを試す時はまず最小構成で動くものを作るに限ると思っています。実行環境テンプレートとして配布されているsubstrate-node-templateには初めからpallet-templateというpalletの雛形が組み込まれており、まさに読み書きを試す最小構成となっています。

コード全文

substrateは発展スピードが凄くてすぐにコードが変わってしまうのですが、ここではv0.9.31を使用しています。palletは専用のマクロを使うことで構成要素を定義していきます。このマクロを切り口として、pallet-templateを詳しく見ていきましょう。言語はRustです。

https://github.com/substrate-developer-hub/substrate-node-template/blob/polkadot-v0.9.31/pallets/template/src/lib.rs
#![cfg_attr(not(feature = "std"), no_std)]

/// Edit this file to define custom logic or remove it if it is not needed.
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://docs.substrate.io/reference/frame-pallets/>
pub use pallet::*;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;

#[frame_support::pallet]
pub mod pallet {
    use frame_support::pallet_prelude::*;
    use frame_system::pallet_prelude::*;

    #[pallet::pallet]
    #[pallet::generate_store(pub(super) trait Store)]
    pub struct Pallet<T>(_);

    /// Configure the pallet by specifying the parameters and types on which it depends.
    #[pallet::config]
    pub trait Config: frame_system::Config {
        /// Because this pallet emits events, it depends on the runtime's definition of an event.
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
    }

    // The pallet's runtime storage items.
    // https://docs.substrate.io/main-docs/build/runtime-storage/
    #[pallet::storage]
    #[pallet::getter(fn something)]
    // Learn more about declaring storage items:
    // https://docs.substrate.io/main-docs/build/runtime-storage/#declaring-storage-items
    pub type Something<T> = StorageValue<_, u32>;

    // Pallets use events to inform users when important changes are made.
    // https://docs.substrate.io/main-docs/build/events-errors/
    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> {
        /// Event documentation should end with an array that provides descriptive names for event
        /// parameters. [something, who]
        SomethingStored(u32, T::AccountId),
    }

    // Errors inform users that something went wrong.
    #[pallet::error]
    pub enum Error<T> {
        /// Error names should be descriptive.
        NoneValue,
        /// Errors should have helpful documentation associated with them.
        StorageOverflow,
    }

    // Dispatchable functions allows users to interact with the pallet and invoke state changes.
    // These functions materialize as "extrinsics", which are often compared to transactions.
    // Dispatchable functions must be annotated with a weight and must return a DispatchResult.
    #[pallet::call]
    impl<T: Config> Pallet<T> {
        /// An example dispatchable that takes a singles value as a parameter, writes the value to
        /// storage and emits an event. This function must be dispatched by a signed extrinsic.
        #[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
        pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResult {
            // Check that the extrinsic was signed and get the signer.
            // This function will return an error if the extrinsic is not signed.
            // https://docs.substrate.io/main-docs/build/origins/
            let who = ensure_signed(origin)?;

            // Update storage.
            <Something<T>>::put(something);

            // Emit an event.
            Self::deposit_event(Event::SomethingStored(something, who));
            // Return a successful DispatchResultWithPostInfo
            Ok(())
        }

        /// An example dispatchable that may throw a custom error.
        #[pallet::weight(10_000 + T::DbWeight::get().reads_writes(1,1).ref_time())]
        pub fn cause_error(origin: OriginFor<T>) -> DispatchResult {
            let _who = ensure_signed(origin)?;

            // Read a value from storage.
            match <Something<T>>::get() {
                // Return an error if the value has not been set.
                None => return Err(Error::<T>::NoneValue.into()),
                Some(old) => {
                    // Increment the value read from storage; will error in the event of overflow.
                    let new = old.checked_add(1).ok_or(Error::<T>::StorageOverflow)?;
                    // Update the value in storage with the incremented result.
                    <Something<T>>::put(new);
                    Ok(())
                },
            }
        }
    }


#[pallet]

palletを定義する最初のマクロ。pallet moduleを定義します。
#[frame_support::pallet]
pub mod pallet {
pallet-templateではframe_supportから呼び出しています。

#[pallet::pallet]

runtimeにpalletを組み込む際に使われる構造体を定義するためのマクロ。

palletでstorageも定義している場合は、#[pallet::generate_store(pub(super) trait Store)] も合わせて使います。
	#[pallet::pallet]
	#[pallet::generate_store(pub(super) trait Store)]
	pub struct Pallet<T>(_);

#[pallet::config]

palletで使うパラメータや型を定義します。
	/// Configure the pallet by specifying the parameters and types on which it depends.
	#[pallet::config]
	pub trait Config: frame_system::Config {
		/// Because this pallet emits events, it depends on the runtime's definition of an event.
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
	}

pallet-templateではEventを発火する機能があるので、このようにSubstrateシステムのEventを使ってRuntimeEventを定義しています。

 

#[pallet::storage]

storageを定義するマクロ。ブロックチェーン上でデータを読み書きできるようになります。

#[pallet::getter(fn $getter_name)]を付けることでgetterを自動生成できます。
	// The pallet's runtime storage items.
	// https://docs.substrate.io/main-docs/build/runtime-storage/
	#[pallet::storage]
	#[pallet::getter(fn something)]
	// Learn more about declaring storage items:
	// https://docs.substrate.io/main-docs/build/runtime-storage/#declaring-storage-items
	pub type Something<T> = StorageValue<_, u32>;
ここではu32型の単一のデータ領域を定義しています。

#[pallet::event]

eventを定義するマクロ。eventはブロックチェーンに記録され、ユーザになんらかの通知を行うために使われます。

#[pallet::generate_deposit($visibility fn deposit_event)]で、eventを発火するための関数deposit_eventを自動生成できます。
	// Pallets use events to inform users when important changes are made.
	// https://docs.substrate.io/main-docs/build/events-errors/
	#[pallet::event]
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
	pub enum Event<T: Config> {
		/// Event documentation should end with an array that provides descriptive names for event
		/// parameters. [something, who]
		SomethingStored(u32, T::AccountId),
	}
ここでは、u32型の値とAccountIdを含むeventを定義しています。

#[pallet::error]

errorを定義するマクロ。あらゆるエラーケースをハンドリングし、ここで定義したerrorを返すことで、実行が中止されることを防ぎます。
// Errors inform users that something went wrong.
	#[pallet::error]
	pub enum Error<T> {
		/// Error names should be descriptive.
		NoneValue,
		/// Errors should have helpful documentation associated with them.
		StorageOverflow,
	}

#[pallet::call]

つまり外部から呼び出されブロックチェーンに状態遷移を引き起こす処理を定義するマクロ。Substrateの文脈ではこのような処理をextrinsicと呼ばれます。
	// Dispatchable functions allows users to interact with the pallet and invoke state changes.
	// These functions materialize as "extrinsics", which are often compared to transactions.
	// Dispatchable functions must be annotated with a weight and must return a DispatchResult.
	#[pallet::call]
	impl<T: Config> Pallet<T> {
また#[pallet::weight($ExpressionResultingInWeight)]で、各関数にweight、つまり計算コストを設定します。
		/// An example dispatchable that takes a singles value as a parameter, writes the value to
		/// storage and emits an event. This function must be dispatched by a signed extrinsic.
		#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
		pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResult {
			// Check that the extrinsic was signed and get the signer.
			// This function will return an error if the extrinsic is not signed.
			// https://docs.substrate.io/main-docs/build/origins/
			let who = ensure_signed(origin)?;

			// Update storage.
			<Something<T>>::put(something);

			// Emit an event.
			Self::deposit_event(Event::SomethingStored(something, who));
			// Return a successful DispatchResultWithPostInfo
			Ok(())
		}
この関数do_somethingは、呼び出し元の署名を検証し、パラメータのu32型の値をstorageに格納し、eventを発火するという処理を行います。
weightは定数+書き込み処理が一回分となっています。

 
		/// An example dispatchable that may throw a custom error.
		#[pallet::weight(10_000 + T::DbWeight::get().reads_writes(1,1).ref_time())]
		pub fn cause_error(origin: OriginFor<T>) -> DispatchResult {
			let _who = ensure_signed(origin)?;

			// Read a value from storage.
			match <Something<T>>::get() {
				// Return an error if the value has not been set.
				None => return Err(Error::<T>::NoneValue.into()),
				Some(old) => {
					// Increment the value read from storage; will error in the event of overflow.
					let new = old.checked_add(1).ok_or(Error::<T>::StorageOverflow)?;
					// Update the value in storage with the incremented result.
					<Something<T>>::put(new);
					Ok(())
				},
			}
		}
2つ目の関数cause_errorでは、呼び出し元の署名を検証し、まだstorageに値が格納されていなければエラーを返し、そうでなければ値をインクリメントします。

挙動

ここから、nodeを起動し、サンプルのフロントエンドアプリを介して挙動を確認していきます。


アプリを立ち上げるとこのような画面になります。ブロックが生成されていく様子がリアルタイムに観察できます。予めいくつかのアカウントと残高が用意されています。

 


その下には送金やruntimeをアップグレードする箇所がありますが、今回はPallet InteractorとEventsを使用します。

 


Interaction TypeのラジオボタンでExtrinsicを選択します。palletのfunctionのセレクトボックスからtemplateModuleを、callableのセレクトボックスからdoSomethingを選択します。呼び出す関数が引数を取る場合は、その入力欄も自動的に出現します。ここではu32型の上限値である4294967295をセットしてみたいと思います。

 


signedで実行すると、すぐにトランザクションがブロックに取り込まれました。Events欄から、確かにeventが発火していることがわかります。

 


Queryのラジオボタンを選択し自動生成されたgetterを呼んでみると、確かに先ほどの引数が格納されていることがわかります。

 


causeErrorを実行してみると、想定通りにオーバーフローを起こしてエラーとなりました。

以上のように、palletを使うことでブロックチェーン上で読み書きが出来ます。

高度な処理

ここまででpalletで出来ることを確認してきましたが、これだけではスマートコントラクトでも出来ることなので際立ったメリットが見えてきません。しかし実はpalletではより高度な処理を実現するマクロが用意されているのでご紹介します。

#[pallet::inherent]

Inherent transactionという特別な処理を定義するマクロ。Blockを生成するnodeが直接データを格納するために使います。
	#[pallet::inherent]
	impl<T: Config> ProvideInherent for Pallet<T> {
		type Call = Call<T>;
		type Error = InherentError;
		const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;

		fn create_inherent(data: &InherentData) -> Option<Self::Call> {
			let inherent_data = data
				.get_data::<InherentType>(&INHERENT_IDENTIFIER)
				.expect("Timestamp inherent data not correctly encoded")
				.expect("Timestamp inherent data must be provided");
			let data = (*inherent_data).saturated_into::<T::Moment>();

			let next_time = cmp::max(data, Self::now() + T::MinimumPeriod::get());
			Some(Call::set { now: next_time })
		}
https://paritytech.github.io/substrate/master/src/pallet_timestamp/lib.rs.html

pallet-timestampでは、inherentを利用してブロックが生成される度にtimestampを記録するという処理を実現しています。

#[pallet::hooks]

様々なタイミングで発火するhookを定義するマクロ。

以下の種類があります。
  • on_initialize: blockのinitialize中に実行される
  • on_idle: blockのfinalizeの前、まだblockのweightに余裕があれば実行される
  • on_finalize: blockのfinalize中に実行される
  • on_runtime_upgrade: runtimeのupdaradeが発生した際に実行される
  • offchain_worker: block生成後にoff-chainで実行される。on-chainのデータを読むことはできるが、書き込むには別途トランザクションを発行する必要がある。時間のかかる処理であったり、HTTPリクエストを発行するといった非決定的な動作も実装できる
非常に強力な機構ですね。実際に、on_initializeの挙動を試してみたいと思います。

 
	#[pallet::hooks]
	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
		fn on_initialize(n: T::BlockNumber) -> Weight {
			<Something<T>>::put(n.saturated_into::<u32>());
			Weight::zero()
		}
	}
pallet-templateに、このようなコードを追加してみました。on_initializeではblock numberが引数として与えられるので、これをそのままstorage領域に書き込んでみます。

 


block numberが18のときに、、、

 


うおっ!ちゃんとstorage領域の値も18になっています!!!自動的にブロックへの書き込みを行うことができる、実に画期的です。

例えばブロック生成間隔が6秒のとき、10ブロックごとに何らかの処理を行うように実装すれば、約1分間隔の定期処理が実現できます。

 


https://coin.z.com/jp/column/buy-ethereum/

 

こういう処理が、オンチェーンのロジックのみで本当に実現できるわけです!!

 

まとめ

palletで何ができるのかを明らかにしてきました。スマートコントラクトと違い、ブロックチェーンのシステムに近い高度な処理を実装できることが分かりました。出来ることが増える分開発者の責任範囲も増えるとも言えますが、誰でも簡単にデプロイして相互にやり取りできるスマートコントラクトと比べればattack surfaceが少なくセキュリティリスクが大きく増える訳でもなさそうです。定期処理が可能になることで、より純粋な分散アプリケーションが実現できるのではないでしょうか。激熱です。

 

グループ研究開発本部 次世代システム研究室では、最新のテクノロジーを調査・検証しながらインターネット上の高度なアプリケーション開発を行うエンジニア・アーキテクトを募集しています。募集職種一覧 からご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事