Substrate区块链框架架构解析与实战指南
发布时间:2026/9/28 23:04:05来源:尧图网络
1. 项目概述Substrate 不是“另一个区块链框架”而是一套可组合的系统工程方法论最近在多个技术社区和开发者聚会里只要聊到区块链底层构建、链上逻辑定制或跨链基础设施substrate这个词几乎必然出现——它不像以太坊那样自带生态共识也不像 Cosmos SDK 那样强绑定 Tendermint更不是一句“用 Rust 写的区块链框架”就能概括清楚的东西。我从 2019 年 Polkadot 白皮书发布时就开始跟进 Substrate参与过三个主网上线前的 runtime 开发、两个平行链的 pallet 定制、以及一次从零搭建 permissioned 链用于政务数据存证的完整交付。实话说substrate 的核心价值从来不在“开箱即用”而在于它把区块链系统中原本耦合在一块的共识、状态机、网络、RPC、存储、升级机制全部解耦成可插拔、可替换、可测试的模块单元。你拿到的不是一个“模板链”而是一套经过生产验证的系统级构建工具链它允许你只写业务逻辑pallet其余部分要么复用标准实现如 FRAME 中的frame-system、frame-balances要么按需替换比如把grandpa换成aura 自定义 finality gadget或者把sc-client替换为轻量级 WASM 执行器。这种设计哲学直接决定了它的适用边界它不适合想“三天发一条链”的营销型项目但极其适合需要长期演进、合规可控、多链协同的企业级链、联盟链、主权链甚至非链场景比如用其 storage layer 构建高一致性分布式配置中心。如果你正在评估是否采用 Substrate关键不是问“它能不能做 XXX”而是问“我的系统对可升级性、状态迁移安全性、执行环境隔离性、治理路径透明度的要求有多高”——这些才是 Substrate 真正发力的地方。2. 核心架构拆解为什么 Substrate 要把区块链拆成 Runtime、Client、Network、Executor 四层2.1 Runtime 层WASM 二进制承载的“链上操作系统内核”Substrate 的 Runtime 不是传统意义上的“智能合约”而是一个编译为 WASM 的、全链共识验证的确定性状态转换函数。它由一组 pallet功能模块组成每个 pallet 封装了特定领域逻辑如账户余额、资产发行、投票治理、时间调度并通过decl_storage!和decl_event!宏声明存储结构与事件接口。关键点在于Runtime 是链的唯一可信执行上下文所有外部调用交易、区块头验证、最终性证明都必须经由它完成。我曾在一个政务链项目中将pallet-treasury改造成支持多级财政审批流改动仅限于 pallet 内部逻辑无需触碰 client 或 network 层——这正是 Runtime 层解耦的价值。WASM 的选择并非为了“跨平台”而是为了安全沙箱执行被严格限制在内存页内无文件系统、无网络、无系统调用所有对外交互如读取区块时间、查询其他 pallet 存储都通过预定义的 host function 接口完成。这意味着 Runtime 可以在任何支持 WASM 的环境中运行浏览器、服务端、嵌入式设备也为后续的“链下计算链上验证”模式打下基础。一个常被忽略的事实是Substrate Runtime 编译出的 WASM blob 是链的“宪法”它决定了链的全部行为规则而链的 genesis state创世状态则是这部宪法的首次执行快照。二者共同构成链的不可篡改起点。2.2 Client 层状态同步与本地验证的“链下协处理器”Client 层是 Substrate 节点的“大脑”负责管理本地数据库、执行区块同步、触发 Runtime 验证、维护权威集视图。它不直接处理交易而是将交易打包、广播、接收新区块后交由 Runtime 执行验证。这里的关键设计是State Trie默克尔帕特里夏树与 Block Execution Pipeline 的分离Client 维护完整的状态树但每次区块执行时Runtime 只接收该区块的输入状态根state root并在内存中重建所需子树进行验证执行完毕后生成新状态根。这种设计让 Client 可以异步加载状态、并行验证多个区块、甚至支持轻客户端light client只同步区块头和必要证明。我在调试一个高吞吐链时发现当 Runtime 执行耗时波动较大时Client 的import_queue会自动降速避免内存溢出——这是 Client 层内置的背压机制在起作用。Client 还负责管理“权威集变更”当 Runtime 通过pallet-session提交新的 validator set 时Client 会解析并更新本地共识模块的验证者列表确保后续区块签名验证正确。这种“链上决策、链下执行”的分工让链的治理升级如更换共识算法变得可预测、可审计、可回滚。2.3 Network 层基于 libp2p 的“去中心化消息总线”Substrate 的 Network 层基于 Rust 实现的 libp2p但它做了大量面向区块链场景的定制自定义协议标识符protocol ID、区块/交易广播的 gossip 优化、权威节点发现机制、以及与 Client 层的深度集成。例如当一个节点收到新区块时Network 层不会简单地泛洪广播而是先检查该区块头是否被本地 Client 认为“可能有效”pre-check再决定是否转发给邻居对于交易它支持“已知交易池”过滤避免重复传播。更重要的是Network 层暴露了NetworkService接口允许 Runtime pallet 直接注册自定义协议比如pallet-bridge用它实现跨链消息通道。我在开发一个跨链桥时就是通过扩展 Network 层协议在两个 Substrate 链之间建立专用的 P2P 通道绕过通用 gossip 网络显著降低了消息延迟。libp2p 的 multiaddr 地址格式/ip4/192.168.1.100/tcp/30333/p2p/Qm...也成了 Substrate 节点的“网络身份证”使得节点发现、连接管理、NAT 穿透等复杂问题被标准化封装。2.4 Executor 层WASM 运行时的“安全执行引擎”Executor 层是 Substrate 的“最后一道防线”它负责加载、实例化、执行 Runtime 的 WASM 代码。Substrate 默认使用wasmiWebAssembly Interpreter作为开发期执行器因为它启动快、调试友好生产环境则推荐wasmtime基于 Cranelift 的 JIT 编译器性能提升约 3–5 倍。Executor 的核心职责有三一是内存隔离为每个 Runtime 实例分配独立线性内存空间防止越界访问二是host function 注入将 Client 提供的系统能力如ext_storage_get、ext_crypto_blake2_256以函数表形式注入 WASM 环境三是超时控制对单次 Runtime 调用设置硬性执行时间上限默认 2 秒超时则强制终止并标记区块无效。我曾遇到一个 pallet 因递归深度过大导致 WASM 栈溢出Executor 的 panic 捕获机制让节点能优雅降级返回错误而非崩溃这比直接 segfault 友好得多。值得注意的是Executor 层还支持“原生执行”native execution当 Runtime 以 native binary 形式编译时Executor 可跳过 WASM 解释/编译步骤直接调用 native 函数——这在本地开发、单元测试、benchmarking 场景下极大提升了效率。3. 实操核心环节从零构建一个带自定义 pallet 的 Substrate 链3.1 环境准备与依赖安装避开 Rust 工具链的“坑中坑”Substrate 开发对 Rust 版本有严格要求目前稳定版v33.x要求 Rust 1.75且必须启用wasm32-unknown-unknowntarget。很多人卡在第一步rustup target add wasm32-unknown-unknown报错。这不是 Rust 本身的问题而是因为某些 Linux 发行版如 Ubuntu 22.04的默认gcc版本过低无法链接 WASM 目标。实操方案是先运行rustup update升级到最新 stable再执行rustup default stable确保主工具链正确最后用rustup target add wasm32-unknown-unknown --toolchain stable显式指定 toolchain。另外cargo-contract和substrate-contracts-node是两个不同东西前者是 ink! 合约的编译/部署工具后者是专为合约优化的轻量节点不要混淆。我建议初学者直接使用官方substrate-node-templatev7.0它已预置了frame-support、frame-system等基础 pallet并配置好 CI/CD 模板。克隆后执行cargo build --release首次编译约需 15–20 分钟取决于 CPU 核数成功后会在target/release/node-template生成可执行文件。注意不要用--dev参数启动就认为万事大吉——--dev模式禁用所有安全检查如 WASM 验证、区块时间戳校验仅用于本地快速验证逻辑上线前必须切换到--chaindev并启用完整验证。3.2 创建自定义 pallet以“链上投票”为例的完整流程假设我们要添加一个pallet-vote支持提案、投票、计票、执行四阶段。第一步是生成 pallet 模板cargo new --lib pallet-vote然后在Cargo.toml中添加依赖[dependencies] frame-support { version 7.0, default-features false, features [std] } frame-system { version 7.0, default-features false, features [std] } sp-runtime { version 33.0, default-features false, features [std] } scale-info { version 2.10, default-features false, features [derive] }关键点在于default-features falseSubstrate pallet 必须支持 no_std无标准库环境因此所有依赖都要显式关闭 std feature。接着在src/lib.rs中定义存储项#[frame_support::pallet] pub mod pallet { use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*}; use frame_system::pallet_prelude::*; #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: FromEventSelf IsTypeSelf as frame_system::Config::RuntimeEvent; type MaxProposals: Getu32; } #[pallet::storage] #[pallet::getter(fn proposals)] pub type ProposalsT: Config StorageMap_, Blake2_128Concat, u32, ProposalT::AccountId, ValueQuery; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum EventT: Config { Proposed { proposal_id: u32 }, Voted { proposal_id: u32, voter: T::AccountId, vote: bool }, } }这里StorageMap使用Blake2_128Concat作为键哈希函数而非 SHA256——这是 Substrate 的性能优化128 位哈希足够防碰撞且计算更快。ValueQuery表示查询不存在键时返回默认值如None避免手动处理Option。编译时若报错cannot find derive macro Clone说明漏加了#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)]这是 SCALE 编码必需的 trait。一个血泪教训pallet 中所有 public 类型struct、enum必须实现Encode/Decode否则 Runtime 编译失败而TypeInfo是用于元数据生成缺失会导致前端无法解析事件参数。3.3 Runtime 集成与类型注册让 pallet “活”在链上将 pallet 加入 Runtime 需要三步一是在runtime/src/lib.rs的construct_runtime!宏中声明 pallet 实例construct_runtime!( pub enum Runtime where Block Block, NodeBlock node_template_runtime::Block, UncheckedExtrinsic UncheckedExtrinsic { System: frame_system::{Pallet, Call, Config, Storage, EventT}, // ... 其他 pallet Vote: pallet_vote::{Pallet, Call, Storage, EventT, ConfigT}, } );二是为 pallet 添加Config实现impl pallet_vote::Config for Runtime { type RuntimeEvent RuntimeEvent; type MaxProposals ConstU32100; }三是注册 pallet 的RuntimeCall类型——这是 Substrate 的关键设计所有 pallet 的调用都被统一为RuntimeCall枚举的一个变体前端通过api.tx.vote.propose(...)发送交易时实际是构造RuntimeCall::Vote(pallet_vote::Call::propose {...})并序列化。务必注意RuntimeCall的大小直接影响交易体积和验证开销因此要避免在 Call 枚举中引入大结构体推荐将复杂参数封装进 storage itemCall 中只传 ID。最后运行cargo check -p node-template-runtime验证编译成功后执行cargo run --release -- --dev --tmp启动节点。用 Polkadot JS Apps 连接ws://localhost:9944在Developer Extrinsics中选择votepallet就能看到propose、vote等可调用函数——此时 pallet 已真正“上线”。3.4 前端交互与事件监听从链上到 UI 的完整闭环Substrate 的前端交互依赖polkadot/api库它通过 WebSocket 连接节点 RPC 接口。初始化 APIimport { ApiPromise, WsProvider } from polkadot/api; const provider new WsProvider(ws://127.0.0.1:9944); const api await ApiPromise.create({ provider });发送交易// 构造 propose 调用 const proposal api.tx.vote.propose(Qm...); // CID of proposal content // 签名并发送 await proposal.signAndSend(account, ({ status }) { if (status.isInBlock) { console.log(Included in block, status.asInBlock.toHex()); } else if (status.isFinalized) { console.log(Finalized, status.asFinalized.toHex()); } });监听事件api.query.system.events((events) { events.forEach(({ event: { data, method, section } }) { if (section vote method Proposed) { const [proposalId] data; console.log(New proposal: ${proposalId.toNumber()}); } }); });关键细节data是VecCodec类型必须根据事件定义的字段顺序解构toNumber()仅适用于u32若为AccountId则需用toString()获取 SS58 地址。我曾因未处理data的类型转换导致前端显示undefined排查了两小时才发现事件参数是AccountId32而非AccountId。另外Polkadot JS Apps 的Developer Events标签页是调试利器它实时显示所有链上事件包括 pallet 内部deposit_event!触发的事件比日志更直观。4. 关键配置与参数调优影响链稳定性与性能的 7 个硬核参数4.1 区块时间与出块速率slot_duration与epoch_length的协同设计Substrate 的 BABE 共识中slot_duration时隙长度和epoch_length纪元长度是两个基础参数。slot_duration通常设为 6 秒Polkadot 主网表示每 6 秒产生一个时隙epoch_length设为 4 * 3600 14400即 4 小时表示每 4 小时重新选举 validator。关键约束是epoch_length必须是slot_duration的整数倍且不能过小否则频繁轮换增加网络开销也不能过大否则惩罚滞后。在测试网中我将slot_duration设为 3 秒、epoch_length设为 3600结果发现 validator 在 epoch 切换时出现短暂失联——原因是 epoch 切换涉及密钥轮换和状态同步3600 个 slot3 小时太短节点来不及完成同步。最终调整为slot_duration6、epoch_length14400稳定运行超过半年。另一个参数expected_block_time期望区块时间应等于slot_duration它影响 RPC 接口rpc.chain.getBlock返回的timestamp字段精度。4.2 存储与状态增长max_state_size与block_weight的平衡术Substrate 用Weight权重量化计算资源消耗每个交易/区块有weight_limit上限。block_weight默认为2_000_000_00020 亿 weight对应约 2 秒 CPU 时间。但权重只是 CPU 消耗真正的瓶颈往往是存储 I/O 和内存占用。max_state_size参数默认 64MB限制单个区块可修改的状态总量超过则拒绝打包。我在一个 NFT 链项目中因单次 mint 操作写入大量 metadata导致区块状态膨胀频繁触发max_state_size限制。解决方案是将 metadata 存储在 IPFS链上只存 CID同时在 pallet 中用StorageMap替代StorageValue避免单 key 过大。此外db_cache_sizeRocksDB 缓存大小需根据服务器内存调整16GB 内存服务器建议设为2GB过小导致频繁磁盘读取过大挤占 Runtime 内存。4.3 网络与同步sync_mode与pruning的生产级配置sync_mode有两个选项Fast快速同步只下载区块头和状态根按需请求状态和Full全量同步下载所有区块体和状态。生产环境必须用Fast否则同步时间长达数天。但Fast模式依赖其他节点提供状态因此需配置--sync-threshold 100当落后 100 区块时强制切换为 full sync。pruning参数决定历史状态保留策略archive存档所有状态磁盘占用最大、1000只保留最近 1000 个区块状态、auto自动清理。对于需要支持历史查询的链如浏览器、区块分析必须用archive对于仅需最新状态的验证节点1000足够。我曾因误配pruning1000导致区块浏览器无法查询 1000 区块前的数据客户投诉后紧急回滚。4.4 WASM 执行与安全wasm_runtime_overrides与max_wasm_stack的防御性设置max_wasm_stack控制 WASM 执行栈大小默认1MB。若 pallet 中存在深度递归如树遍历可能触发栈溢出。安全做法是在node/src/service.rs中显式设置max_wasm_stack: 2 * 1024 * 10242MB。wasm_runtime_overrides允许为特定 Runtime 版本指定 native 实现提升性能。例如为runtime_version: 100的链指定native_runtime: node-template-native当节点检测到匹配版本时自动跳过 WASM 解释直接调用 native 函数。这在 benchmarking 时至关重要cargo run --release --features runtime-benchmarks -- benchmark --chaindev --steps50 --repeat20 --pallet pallet-vote --extrinsic *会生成 native benchmark 结果比 WASM 快 10 倍以上。4.5 RPC 与监控rpc_methods与prometheus-external的权限管控rpc_methods参数控制 RPC 接口暴露范围Unsafe暴露所有接口含author_insertKey、Safe仅读取接口、Auto根据--unsafe-rpc-external自动判断。生产环境必须设为Safe并用反向代理如 nginx限制 IP 白名单。prometheus-external启用 Prometheus 监控指标导出默认端口9615。关键指标包括substrate_block_import_elapsed_seconds区块导入耗时、substrate_runtime_execution_time_secondsRuntime 执行耗时、substrate_network_peers_connected连接节点数。我通过 Grafana 面板监控substrate_runtime_execution_time_seconds{quantile0.99}当 99 分位耗时超过 1.5 秒时自动告警并触发 pallet 性能分析。4.6 升级与治理code_hash与scheduledpallet 的灰度发布机制Substrate 的 Runtime 升级通过set_code调用实现但直接升级风险极高。最佳实践是结合pallet-scheduler和pallet-democracy实现灰度发布先提交升级提案经投票通过后scheduler 在指定区块高度执行set_code。升级前需计算新 Runtime 的code_hash./target/release/node-template build-spec --disable-default-bootnode --chaindev raw.json ./target/release/node-template build-spec --chainraw.json --raw --disable-default-bootnode customSpecRaw.json # 提取 code 字段计算 blake2_256 hash将code_hash与提案绑定确保升级内容与预期一致。我曾在一个金融链升级中因未校验code_hash导致误升级了测试版 Runtime造成 2 小时服务中断。此后所有升级流程都加入code_hash人工复核环节。4.7 日志与调试--log级别与tracing的精准定位Substrate 默认日志级别为info但关键模块如txpool、consensus、runtime需设为debug。启动命令./target/release/node-template --dev --log txpooldebug,runtimedebug,consensusdebug更高级的是启用tracing在Cargo.toml中为sc-service添加features [tracing]然后用jaeger收集 span。例如追踪一笔交易从txpool到import_queue再到Runtime的完整路径能精准定位瓶颈在哪个环节。我在优化一个高并发链时通过 tracing 发现 70% 时间消耗在sc-client的execute_block调用上进而聚焦优化 Runtime 中的 storage 读取逻辑。5. 常见问题与实战排障从编译失败到共识卡顿的 12 个真实案例5.1 编译类问题proc-macro与no_std冲突的根源与解法现象cargo build报错error[E0463]: cant find crate for std即使已声明default-features false。根因某个依赖如serde的derive宏内部隐式依赖std而 pallet 的lib.rs未正确配置#![cfg_attr(not(feature std), no_std)]。解法在pallet/src/lib.rs顶部添加#![cfg_attr(not(feature std), no_std)] #![cfg_attr(not(feature std), feature(alloc))]并在Cargo.toml的[dev-dependencies]下添加alloc依赖。经验所有 pallet 必须显式声明no_std否则编译器无法推断。5.2 Runtime 验证失败Invalid Transaction的 3 种细分场景现象交易返回Invalid Transaction但无具体原因。排查路径若TransactionValidityError::Invalid(InvalidTransaction::BadProof)签名验证失败检查AccountId格式与签名私钥是否匹配若InvalidTransaction::Payment交易手续费不足确认pallet-transaction-payment的NextFeeMultiplier未异常飙升若InvalidTransaction::Stale交易 nonce 错误前端需调用system.accountNextIndex获取正确 nonce。5.3 网络同步停滞Not syncing状态的 5 步诊断法现象节点日志显示Not syncing区块高度停滞。诊断步骤curl -H Content-Type: application/json -d {id:1,jsonrpc:2.0,method:system_health,params:[]} http://localhost:9933检查isSyncing字段telnet localhost 30333测试 P2P 端口是否开放ss -tuln | grep 30333确认端口监听状态journalctl -u node-template -f查看 systemd 日志搜索error关键字若network模块报Failed to connect to peer检查--bootnodes参数是否指向有效节点。5.4 WASM 执行超时ExecutionLimitExceeded的性能优化清单现象区块导入失败日志出现ExecutionLimitExceeded。优化项检查 pallet 中是否存在 O(n²) 循环改为BTreeMap或分页查询将大数组VecT替换为StorageMap避免单次读取全量在#[pallet::call]函数中添加ensure!(T as Config::MaxItems::get() items.len(), Error::T::TooManyItems);做前置校验调整block_weight上限但需同步调整MaximumBlockWeight常量。5.5 事件丢失前端监听不到deposit_event!的 4 个盲区现象Runtime 中deposit_event!正常触发但前端api.query.system.events无响应。盲区未启用--ws-max-connections 1000连接数超限被拒绝api实例未等待api.isReady完成即开始监听事件section/method名称大小写不匹配Substrate 生成的事件名全小写data解构顺序错误如事件定义为(AccountId, u32)但代码中写成[u32, AccountId]。5.6 存储膨胀RocksDB占用激增的 3 种清理策略现象/data/chains/dev/db目录每月增长 50GB。策略启用pruning archive时定期运行./target/release/node-template purge-chain --chaindev清理旧链在 pallet 中使用remove_all()替代kill_storage()前者只删 key后者删整个 storage对历史数据启用offchain_worker定期归档到 S3链上只留索引。5.7 共识卡顿BABEEpoch切换失败的 2 个致命配置现象节点日志反复出现Failed to finalize blockepoch无法推进。致命配置epoch_length未被slot_duration整除导致 epoch 边界计算错误initial_authorities中的 validator 地址未在sudo权限下正确注册导致新 epoch 无有效 authority。5.8 前端兼容性Polkadot JS Apps 连接失败的 DNS 与 CORS 陷阱现象浏览器控制台报WebSocket connection to ws://... failed。陷阱本地localhost无法被某些浏览器识别改用127.0.0.1--rpc-cors all仅允许*但现代浏览器要求明确列出域名如--rpc-cors https://polkadot.js.orgDNS 缓存导致旧 IP 生效执行sudo systemd-resolve --flush-caches清理。5.9 升级失败set_code后节点崩溃的 ABI 不兼容检查表现象Runtime 升级后节点 panic日志panic: storage migration failed。检查表新旧 Runtime 的StorageVersion是否递增on_runtime_upgrade函数是否正确处理旧 storage schemaGenesisConfig中的storage字段是否与新 pallet 的decl_storage!定义匹配。5.10 交易池拥堵txpool拒绝交易的 3 个阈值参数现象高频交易下txpool拒绝新交易日志Transaction pool is full。阈值--pool-kbytes 10240交易池内存上限默认 10MB--pool-limit 8192最大交易数默认 8192--pool-reject-longevity 1000交易最长存活区块数默认 1000。5.11 跨链消息失败pallet-bridge通道中断的 4 个链路检查点现象跨链消息未送达目标链。检查点源链OutboundChannel的next_outbound_message_nonce是否递增目标链InboundChannel的latest_received_nonce是否同步两条链的Relayer账户余额是否充足BridgeHubpallet 的MessageDelivery事件是否被正确监听。5.12 监控告警误报Prometheus 指标抖动的 2 个采样陷阱现象substrate_block_import_elapsed_seconds指标频繁尖峰但实际无异常。陷阱sc-telemetry的默认采样率过高导致瞬时值失真改用--telemetry-url wss://telemetry.polkadot.io/submit/ 0降低频率Grafana的Min step设置过小如1s应设为30s以平滑噪声。提示所有 Substrate 问题的终极排查法是——查看sc-client模块的日志。它记录了从区块接收、验证、执行到存储的全链路细节比runtime日志更底层、更全面。6. 生产部署与运维从单节点测试到百节点集群的 5 阶段演进6.1 阶段一本地开发与单节点测试Dev此阶段目标是验证 pallet 逻辑正确性。使用--dev启动配合polkadot-js/apps进行交互。关键动作编写单元测试#[cfg(test)]模块覆盖dispatchable函数的 happy path 和 error path运行cargo test -p pallet-vote确保 100% 通过。注意--dev模式下sudo权限默认开启上线前必须移除所有ensure_root!调用改用pallet-sudo的显式调用。6.2 阶段二测试网部署Testnet部署至少 3 个 validator 节点配置--validator参数。使用subkey生成 sr25519 密钥对将public key注册为 validator。关键配置--chaintestnet.json指向自定义链 spec--rpc-external --ws-external开放 RPC--rpc-corsall允许跨域。安全红线禁止在测试网使用主网 seed phrase所有密钥必须隔离生成。6.3 阶段三灰度发布Canary选择 1–2 个可信 validator 节点部署新版本 Runtime观察 24 小时。监控指标block_import_elapsed_seconds99 分位 1.2 秒network_peers_connected 10txpool_size稳定在 500 以下。灰度期间禁止任何sudo操作所有升级通过pallet-democracy提案流程。6.4 阶段四全网升级Production当灰度节点稳定后提交set_code提案设定执行区块高度如current_block 1000。升级前 1 小时通知所有 validator 更新 binary升级后 1 小时检查system.lastRuntimeUpgrade
网站建设高端定制企业官网