目录标题
1. 获取substrate-node-template代码
https://github.com/substrate-developer-hub/substrate-node-template/
获取代码后切换到 polkadot-v0.9.30 (这是一个tag)
2. 添加一个用于测试的ocw-test目录至pallets
对于如何添加一个新的pallet中以参照:添加新的pallet
3. 编写ocw-test/src/lib.rs代码
3.1 需要用到的包名的引用
#![cfg_attr(not(feature = "std"), no_std)]
use frame_system::{
offchain::{AppCrypto, CreateSignedTransaction, SendSignedTransaction, Signer},
pallet_prelude::*,
};
use sp_core::crypto::KeyTypeId;
pub use pallet::*;
3.2 模块crypto的实现
此处KeyTypeId由四个字母组成、在后边做insertKey时使用
pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"xuan");
pub mod crypto {
use super::KEY_TYPE;
use sp_runtime::{
app_crypto::{app_crypto, sr25519},
// traits::Verify,
MultiSignature,
MultiSigner,
};
app_crypto!(sr25519, KEY_TYPE);
pub struct TestAuthId;
impl frame_system::offchain::AppCrypto<MultiSigner, MultiSignature> for TestAuthId {
type RuntimeAppPublic = Public;
type GenericSignature = sp_core::sr25519::Signature;
type GenericPublic = sp_core::sr25519::Public;
}
}
TestAuthId必须实现AppCrypto这个trait,runtime中要用它初始化关联类型,主要用处是指定签名类型。
3.3 mode pallet 的实现-config、storage、event
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + CreateSignedTransaction<Call<Self>> {
type AuthorityId: AppCrypto<Self::Public, Self::Signature>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
#[pallet::storage]
#[pallet::getter(fn something)]
pub type Something<T> = StorageValue<_, u64>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
SendSignedSomething(u64, T::AccountId),
}
3.4 mode pallet 的实现-call
该方法用于调用签名交易成功时执行,将区块号写入storage
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn submit_send_signed_something(
origin: OriginFor<T>,
blocknumber: u64,
) -> DispatchResult {
log::info!("### submit_send_signed_something start...]");
let who = ensure_signed(origin)?;
log::info!(
"### submit_send_signed_something who:[{:?}], number:[{:?}]",
who,
blocknumber
);
<Something<T>>::put(blocknumber);
Self::deposit_event(Event::SendSignedSomething(blocknumber, who));
Ok(())
}
}
3.5 mode pallet 的实现-hook-offchain_worker
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn offchain_worker(block_number: T::BlockNumber) {
let signer = Signer::<T, T::AuthorityId>::any_account();
log::info!("### offchain_worker signer can sign:[{:?}]", signer.can_sign());
let number: u64 = block_number.try_into().unwrap_or(0);
log::info!("### offchain_worker signer number:[{:?}]", number);
let result = signer.send_signed_transaction(|_account| {
Call::submit_send_signed_something { blocknumber: number }
});
if let Some((_acc, res)) = result {
if res.is_err() {
if let Err(e) = res {
log::info!("### Error! send_signed_transaction:[{:?}]", e);
} else {
log::info!("### Error! Unknow100");
}
} else {
log::info!("### Error! Offchain Signed Error!");
}
}else {
log::info!("### Error! NoAccountForSigning!");
}
}
}
4. runtime/src/lib.rs的实现
impl pallet_ocw_test::Config for Runtime {
type AuthorityId = pallet_ocw_test::crypto::TestAuthId;
type RuntimeEvent = RuntimeEvent;
}
use codec::Encode;
use sp_runtime::{generic::Era, SaturatedConversion, traits};
impl<LocalCall> frame_system::offchain::CreateSignedTransaction<LocalCall> for Runtime
where
RuntimeCall: From<LocalCall>,
{
fn create_transaction<C: frame_system::offchain::AppCrypto<Self::Public, Self::Signature>>(
call: RuntimeCall,
public: <Signature as Verify>::Signer,
account: AccountId,
nonce: Index,
) -> Option<(RuntimeCall, <UncheckedExtrinsic as traits::Extrinsic>::SignaturePayload)> {
let tip = 0;
// take the biggest period possible.
let period =
BlockHashCount::get().checked_next_power_of_two().map(|c| c / 2).unwrap_or(2) as u64;
let current_block = System::block_number()
.saturated_into::<u64>()
// The `System::block_number` is initialized with `n+1`,
// so the actual block number is `n`.
.saturating_sub(1);
let era = Era::mortal(period, current_block);
let extra = (
frame_system::CheckNonZeroSender::<Runtime>::new(),
frame_system::CheckSpecVersion::<Runtime>::new(),
frame_system::CheckTxVersion::<Runtime>::new(),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckEra::<Runtime>::from(era),
frame_system::CheckNonce::<Runtime>::from(nonce),
frame_system::CheckWeight::<Runtime>::new(),
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip),
);
let raw_payload = SignedPayload::new(call, extra)
.map_err(|e| {
log::warn!("Unable to create signed payload: {:?}", e);
})
.ok()?;
let signature = raw_payload.using_encoded(|payload| C::sign(payload, public))?;
let address = account;
let (call, extra, _) = raw_payload.deconstruct();
Some((call, (sp_runtime::MultiAddress::Id(address), signature, extra)))
}
}
impl frame_system::offchain::SigningTypes for Runtime {
type Public = <Signature as traits::Verify>::Signer;
type Signature = Signature;
}
impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime
where
RuntimeCall: From<C>,
{
type Extrinsic = UncheckedExtrinsic;
type OverarchingCall = RuntimeCall;
}
代码写完后,编译运行
cargo build
./target/debug/node-template --dev
5. 使用subkey生成公私钥
获取substrate工程代码、编译package subkey,
git clone https://github.com/paritytech/substrate.git
cargo build -p subkey --release
生成密钥
6. 使用polkadot.js.org/apps来测试
打开 https://polkadot.js.org/apps/ 连接本地网络、
6.1 使用RPC calls配置密钥信息
keyType对应代码中的KeyTypeId对应的四位字母;
suri 对应的是生成密钥的第一行Secret phrase;
publicKey对应添入Public key (hex)的值;
提交RPC调用后,可以看到命令行输出的变化:
can sign由false变成了true,但是出现一行错误
### offchain_worker signer can sign:[true]
Transaction pool error: Invalid transaction validity: InvalidTransaction::Payment
这表示账户余额不足所报的错,我们可以使用其它账户转一些balance过来
6.2 向测试账户转账
请注册此处的AccountId需要手动输入上面subkey生成的密钥中的Public key (SS58)对应的值,提交交易后
可以看到命令行中的变化
因为代码中提交成功输出的内容是:
log::info!("### Error! Offchain Signed Error!");
因此这时已经是成功状态了,可以通过界面查看结果
6.2 在poldadot的UI上确认成功交易
可以看到在下方ocwTestModule.something: Option中的数值一直在不断变化,因为每次import一个新的块时,对应的OCW代码都会执行一次,将当前的区块号写入,因此数值会不断增长