ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Substrate Offchain Worker 实战指南:基于 pallet-example-offchain-worker 构建链上价格预言机

Substrate Offchain Worker 实战指南:基于 pallet-example-offchain-worker 构建链上价格预言机 区块链开发框架后端【免费下载链接】substrateSubstrate: The platform for blockchain innovators项目地址https://gitcode.com/gh_mirrors/su/substrate点击查看免费下载导读本文以 Substrate 仓库中的示例 Pallet frame/examples/offchain-worker/README.md 为蓝本完整剖析一个真实可运行的 Offchain WorkerOCW范例它每个区块被触发后通过 HTTP 拉取 BTC/USD 价格再以签名或未签名交易的形式把结果回写链上由链上逻辑聚合最近 64 个价格并计算均值。读完本文你将掌握 OCW 的核心概念offchain_workerhook、Local Storage 节流、签名/未签名交易提交、ValidateUnsigned白名单校验并能直接对照源码理解一个完整预言机 Pallet 的工程骨架。示例背景与定位该 Pallet 的包名为pallet-example-offchain-worker见 frame/examples/offchain-worker/Cargo.toml被官方定位为A simple pallet demonstrating concepts, APIs and structures common to most offchain workers.它用一个非常简陋、幼稚、绝不生产可用的 BTC/USD 预言机来演示 OCW 的常见概念、API 与代码组织方式。官方在 README 与源码注释中反复强调该 Pallet 仅用于教学展示不可用于生产环境。整体数据流如下每个区块导入成功后offchain_workerhook 被触发OCW 通过sp_io::offchain提供的高层 HTTP API 向公开价格接口发起 GET 请求拿到价格后OCW 根据区块号选择签名交易或未签名交易把价格提交回链链上逻辑把价格写入有界存储Prices只保留最近MaxPrices示例中为 64个值并据此计算平均价为防止刷屏OCW 使用 Local Storage 做节流GracePeriod未签名路径则用NextUnsignedAt存储项与ValidateUnsigned双重限频。一、Offchain Worker 的运行机制1.1 入口offchain_workerhook任何 Pallet 只要实现了 frame/support/src/traits/hooks.rs 中的Hooks::offchain_worker就声明了一个 offchain worker。该 hook 的特性源码注释明确说明在节点完全同步且成功导入一个新的 best 区块后调用不保证每个区块都执行某些区块可能被跳过某些区块如发生 re-org 时可能执行两次因此代码必须能够容忍这种不确定性通常用 Local Storage API 协调多次运行它运行在独立的线程中与主 WASM 运行时完全解耦可以执行长时间任务如 HTTP 调用可以自由读取链上状态但对状态的任何直接修改都无意义——写回链上的唯一途径是把交易提交到交易池它额外获得sp_io::offchain提供的一组 host functionsHTTP、Local Storage、随机数等这些在普通运行时执行路径中不可用。示例中的入口实现frame/examples/offchain-worker/src/lib.rs#L164-L213#[pallet::hooks] implT: Config HooksBlockNumberForT for PalletT { fn offchain_worker(block_number: BlockNumberForT) { log::info!(Hello World from offchain workers!); // OCW 可以直接读取链上存储例如取父区块哈希 let parent_hash system::PalletT::block_hash(block_number - 1u32.into()); // 读取当前链上平均价 let average: Optionu32 Self::average_price(); // 根据区块号决定发送哪种类型的交易 let should_send Self::choose_transaction_type(block_number); let res match should_send { TransactionType::Signed Self::fetch_price_and_send_signed(), TransactionType::UnsignedForAny Self::fetch_price_and_send_unsigned_for_any_account(block_number), TransactionType::UnsignedForAll Self::fetch_price_and_send_unsigned_for_all_accounts(block_number), TransactionType::Raw Self::fetch_price_and_send_raw_unsigned(block_number), TransactionType::None Ok(()), }; if let Err(e) res { log::error!(Error: {}, e); } } }代码注释里给出的工程实践建议保持offchain_worker函数本身最小化把主要逻辑移到单独的impl块中本示例正是如此组织同时注意编译进 WASM 的日志会显著增大 blob 体积可用RuntimeDebug派生宏隐藏细节sp-apicrate 还提供disable-loggingfeature 可彻底移除 WASM 中的日志。1.2 节点侧如何调度 OCW节点侧由 client/offchain/src/lib.rs 中的OffchainWorkers驱动它订阅区块导入通知流只在is_new_best true时派发任务非 canonical 区块会被跳过并在独立的线程池中执行。相关 CLI 参数定义在 client/cli/src/params/offchain_worker_params.rs--offchain-worker ENABLED取值WhenAuthority默认仅验证人节点执行/Always/Never--enable-offchain-indexing启用 Offchain Indexing API允许区块导入期间直接写入 offchain DB。1.3 配置项Config 常量示例通过#[pallet::constant]暴露了四个配置参数lib.rs#L124-L159配置项类型作用测试中的取值GracePeriodGetBlockNumberForSelf发送交易后的宽限期在宽限期内不再发送用 Local Storage 协调ConstU645UnsignedIntervalGetBlockNumberForSelf未签名交易的最小间隔区块数保证每UnsignedInterval个区块只接受一次未签名交易ConstU64128UnsignedPriorityGetTransactionPriority未签名交易的基础优先级便于在多个 Pallet 都发未签名交易时调优1 20MaxPricesGetu32链上保存的价格条目上限ConstU3264此外Config还要求AuthorityId: AppCryptoSelf::Public, Self::Signature应用级密钥类型以及RuntimeEvent。测试运行时的完整实现见 tests.rs#L105-L116。二、密钥管理与签名基础2.1 应用级密钥AppCrypto每个处理签名的模块都需要声明自己唯一的KeyTypeIdOCW 签名时会从 keystore 中请求该类型的所有密钥来签名pub const KEY_TYPE: KeyTypeId KeyTypeId(*bbtc!); pub mod crypto { app_crypto!(sr25519, KEY_TYPE); pub struct TestAuthId; impl frame_system::offchain::AppCryptoMultiSigner, MultiSignature for TestAuthId { type RuntimeAppPublic Public; type GenericSignature sp_core::sr25519::Signature; type GenericPublic sp_core::sr25519::Public; } }密钥可以手动通过 RPCauthor_insertKey注入也可由 keystore 在本地生成支持sr25519、ed25519、ecdsa等加密方案通过app_crypto!宏与模块专属KeyTypeId绑定防止不同模块之间签名串用。2.2 frame-system 的 offchain 交易基础设施签名/未签名交易的通用能力定义在 frame/system/src/offchain.rs示例主要使用了以下组件SubmitTransactionT, LocalCall直接把 call 编码为交易提交到交易池submit_unsigned_transaction即其未签名便捷方法SignerT, C, X管理用于签名的密钥集合支持all_accounts()使用 keystore 中所有可用密钥、any_account()任意一个密钥、with_filter(...)与给定账号列表取交集并可检查can_sign()SignedPayloadT可签名载荷抽象签名后随未签名交易上链链上通过SignedPayload::T::verify::AuthorityId验证SendTransactionTypes/CreateSignedTransaction运行时必须实现的关联类型与交易构造逻辑。对应的 trait 定义均在 frame/system/src/offchain.rs 中例如SubmitTransaction::submit_transaction底层调用sp_io::offchain::submit_transaction(xt.encode())。三、交易类型与发送路径示例把发送策略按block_number % 4轮换lib.rs#L378-L441一次运行只会选择其中一种交易类型触发条件发送路径链上 CallSignedblock_number % 4 0Signer::all_accounts().send_signed_transaction(...)submit_price需签名付费UnsignedForAny% 4 1Signer::any_account().send_unsigned_transaction(...)submit_price_unsigned_with_signed_payloadUnsignedForAll% 4 2Signer::all_accounts().send_unsigned_transaction(...)submit_price_unsigned_with_signed_payloadRaw% 4 3SubmitTransaction::submit_unsigned_transaction(...)submit_price_unsigned无签名载荷代码注释特别提醒这种取模轮换并不保证公平交替可能连续两次发同类型若要严格交替应改用存储项记录下一笔交易的类型。3.1 Local Storage 节流防止刷屏choose_transaction_type的节流逻辑使用StorageValueRef::persistent(bexample_ocw::last_send)的mutate原子操作底层是 Local Storage 的compare_and_set若上次发送的区块号距今仍在GracePeriod内返回Err(RECENTLY_SENT)本次不发送否则写入当前区块号并返回Ok(block_number)以允许发送若返回MutateStorageError::ConcurrentModification说明并发运行的另一个 OCW 已抢先写入抢到了锁本次同样不发送。Local Storage 在所有 OCW 实例间共享且持久化官方建议在存储键前加模块名前缀示例为example_ocw::避免冲突。3.2 链上 Call 一览Pallet 暴露了三个#[pallet::call]lib.rs#L216-L291submit_price(origin, price)ensure_signed校验签名交易调用入口调用方支付手续费从而抬高刷屏攻击成本submit_price_unsigned(origin, block_number, price)ensure_none校验只能通过未签名交易调用调用后把NextUnsignedAt更新为当前区块 UnsignedIntervalsubmit_price_unsigned_with_signed_payload(origin, price_payload, signature)接受带签名载荷的未签名交易同样更新NextUnsignedAt。三个 call 的 weight 均声明为{0}示例简化处理但源码注释强调未签名调用也必须声明 weight否则单个区块可能被无限数量的未签名交易塞满。3.3 链上存储与聚合逻辑Prices: StorageValueBoundedVecu32, T::MaxPrices有界向量保存最近的价格add_price在try_push失败已满 64 条时用prices[(price % MaxPrices) as usize] price覆盖旧值NextUnsignedAt: StorageValueBlockNumberForT记录下一个未签名交易可被接受的区块号average_price()对所有已存价格求和后取平均saturating_add防溢出每次接受新价格都会触发Event::NewPrice { price, maybe_who }事件。四、HTTP 价格抓取与 JSON 解析4.1 发起请求fetch_pricelib.rs#L573-L627演示了 OCW 的 HTTP 能力// 硬编码 2 秒截止时间控制 OCW 执行时长 let deadline sp_io::offchain::timestamp().add(Duration::from_millis(2_000)); let request http::Request::get(https://min-api.cryptocompare.com/data/price?fsymBTCtsymsUSD); // 发送请求有独立 deadline之后可先做别的事再阻塞等待响应 let pending request.deadline(deadline).send().map_err(|_| http::Error::IoError)?; let response pending.try_wait(deadline).map_err(|_| http::Error::DeadlineReached)??; if response.code ! 200 { return Err(http::Error::Unknown) } let body response.body().collect::Vecu8();要点使用sp_runtime::offchain::http高层封装底层 host 调用在sp_io中API 风格类似reqwest但运行在自定义 WASM 环境中无法直接引入网络库请求与响应读取可以分别设置 deadline也可以并发发起多个请求后统一收结果请求由 host 驱动即使不读取响应它也会自行完成。4.2 解析响应parse_pricelib.rs#L632-L647使用lite-json见 Cargo.toml解析{USD: 155.23}格式的 JSON并把美元价格换算为分整数美分返回例如155.23→15523。测试用例parse_price_workstests.rs#L377-L390验证了整数、小数、多余小数位、缺失键、字符串值等边界情况。五、未签名交易的合法性校验ValidateUnsigned未签名交易默认是被禁止的必须实现#[pallet::validate_unsigned]来白名单特定 calllib.rs#L301-L329。源码注释给出了严厉警告未签名校验逻辑的任何疏漏都可能打开 DoS / 刷屏攻击向量必须极其谨慎。示例的validate_unsigned逻辑对submit_price_unsigned_with_signed_payload先SignedPayload::verify::T::AuthorityId验签失败返回InvalidTransaction::BadProof再进入公共参数校验对submit_price_unsigned直接进入公共参数校验其他 call返回InvalidTransaction::Call。公共校验validate_transaction_parameterslib.rs#L675-L725若NextUnsignedAt block_number来得太早→InvalidTransaction::Stale若block_number 当前区块来自未来→InvalidTransaction::Future构造ValidTransactionpriority UnsignedPriority |平均价 - 新价格|偏离均值越大优先级越高示例仅为演示and_provides(next_unsigned_at)以next_unsigned_at为 tag保证同一槽位只有一个未签名交易能留在交易池并进块高优先级者可替换低优先级者longevity(5)交易仅在未来 5 个区块内有效之后会被池重新校验propagate(true)允许广播给其他对等节点。六、测试与验证单元测试位于 frame/examples/offchain-worker/src/tests.rs使用sp_io::TestExternalities配合三个测试扩展TestOffchainExt模拟 HTTP 请求OffchainWorkerExtprice_oracle_response预置{USD: 155.23}的响应TestTransactionPoolExt捕获提交的交易供断言解码后的 call 与签名KeystoreExt/MemoryKeystore用固定助记词短语生成 sr25519 密钥。代表性用例it_aggregates_the_price验证签名提交 27、43 后平均价为 35should_make_http_call_and_parse_result验证fetch_price返回 15523should_submit_signed_transaction_on_chain断言交易无签名、call 为submit_price { price: 15523 }should_submit_unsigned_transaction_on_chain_for_any_account/_for_all_accounts断言交易signature None且载荷签名可通过SignedPayload::verifyshould_submit_raw_unsigned_transaction_on_chain断言 call 为submit_price_unsigned { block_number: 1, price: 15523 }。如需查看模块的 rustdoc 文档可在仓库根目录运行cargo doc --package pallet-example-offchain-worker --open七、学习路径与延伸本示例是 frame/examples/src/lib.rs 中系列 FRAME 示例之一pallet_example_offchain_worker更基础的示例见 frame/examples/basic/src/lib.rs其注释明确指向本 offchain-worker 示例底层 offchain 交易基础设施Signer、AppCrypto、SubmitTransaction、SignedPayload等 trait 的完整定义与签名可深入阅读 frame/system/src/offchain.rs节点侧调度器与 CLI 开关可参考 client/offchain/src/lib.rs 与 client/cli/src/params/offchain_worker_params.rs结合 frame/support/src/traits/hooks.rs 中offchain_worker的文档注释可以理解 hook 的语义边界与sp_io::offchain的可用 host functions。总而言之pallet-example-offchain-worker是理解 Substrate Offchain Worker 全流程触发 → HTTP 抓取 → 节流 → 签名/未签名提交 → 链上校验与聚合的最佳教学范本生产环境中的真实 OCW 可以在此骨架之上替换更可靠的数据源、更严谨的校验逻辑与更完善的防刷屏策略。赞分享区块链开发框架后端【免费下载链接】substrateSubstrate: The platform for blockchain innovators项目地址https://gitcode.com/gh_mirrors/su/substrate点击查看免费下载相关推荐MAS 完全实战指南Windows 与 Office 激活一步到位MAS 完全实战指南Windows 与 Office 激活一步到位 Word 顶上那行灰字“未授权的产品”赖着不走或者桌面右下角突然冒出“Windows 未操作系统Substrate Asset Conversion Pallet基于恒定乘积公式的链上 AMM 交换模块实战指南Substrate Asset Conversion Pallet基于恒定乘积公式的链上 AMM 交换模块实战指南 Substrate 的 pallet as区块链开发框架后端IsaacLab 模仿学习数据集全流程指南IsaacLab 模仿学习数据集全流程指南 遥操作采集了 30 条演示策略却在「抓取方块」这个子任务上反复失败——问题多半出在标注边界或数据扩展环节。Isaa人工智能强化学习机器人具身智能深度学习创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表