☰
Quip 协议链上矿工数据方案:从 Remark 到 pallet-miner-registry 的架构演进与实践
2026/10/12 4:43:07 网站建设 项目流程

【免费下载链接】quip-validator

A rust implementation of the Quip Protocol forked from Substrate

项目地址:https://gitcode.com/gh_mirrors/qu/quip-validator
点击查看免费下载

导读

本文围绕 Quip 协议(quip-validator仓库,一个 fork 自 Substrate 的 Rust 实现)中“链上矿工数据(on-chain miner data)”的设计方案展开,系统讲解如何把矿工节点描述(node descriptor)与每个 qblock 的参与标记(participation marker)从System.remark明文备注中迁出,落到一个全新的类型化 pallet——pallet-miner-registry。你将掌握:为什么 remark 机制必须被废弃、新 pallet 的存储/事件/外置函数(extrinsic)/运行时 API 如何设计、Python 矿工客户端与索引器的契约如何随之改写,以及完整的实现顺序与测试计划。文中所有运行时设计均以仓库源码为证,可直接对照 pallets/miner-registry/src/lib.rs 与 runtime/src/configs/mod.rs 阅读。


一、总体目标:让矿工、索引器、仪表盘各司其职

方案(原文为docs/quip-protocol-on-chain-miner-data-plan.md,下称“计划”)设定的核心目标是两条:

  1. quip-miner在孤立运行(in isolation)时不应依赖索引器(indexer)——矿工必须能够只对着链本身完成发现、提交与自我校验;
  2. 索引器应尽量少干活,并且不应为同一份数据同时查询矿工与链——能从一个源头拿到的数据就不要设计成两个源头。

由此自然推导出一个重要约束:链上只保存原始事实(raw facts),不计算聚合统计。仪表盘因此大幅简化——它只需要与索引器通信,而索引器负责汇总一切必要数据;参与率、舰队汇总、计数器、图表这类 dashboard 专用 rollup 全部是索引器的职责,运行时绝不添加。

二、现状痛点:为什么 Remark 机制必须被抛弃

当前quip-protocol(Python 矿工客户端,独立于本仓库)使用System.remark_with_event或System.remark提交两类矿工可观测数据:

  • 节点描述(node descriptors):由启动自动识别(startup auto-identify)与quip-miner identify提交;
  • 每个 qblock 的参与标记(participation markers):每个节点、每个 solution number 提交一次。

计划明确指出这套机制应整体废弃,理由有三:

  1. remark 让索引器不得不解析不透明的 JSON 事件——事件体是矿工自行约定的,链上无类型可言;
  2. 回退行为依赖运行时(runtime-dependent fallback)——客户端要猜测链是否支持某个调用路径;
  3. 强制 dashboard/索引器理解矿工本地的私有约定,而不是读取类型化的链状态。

换句话说:可观测数据应当是链的公民,而不是被塞进备注里的匿名字节流。

三、运行时侧:新增 pallet-miner-registry(首选形态)

3.1 为什么是“新 pallet”,而不是扩展现有 pallet

计划明确不把该逻辑塞进pallet-quantum-pow或pallet-quantum-compute-mempool,而是新建一个暂定名为pallet-miner-registry的 pallet,理由如下:

  • 描述(descriptor)同时适用于 PoW 矿工与 mempool 求解者(solvers),横跨两个域;
  • 参与是观测数据,不是 PoW 校验数据,与出块共识无关;
  • 单一 pallet 给索引器提供唯一的类型化链上数据源(one typed chain source for miner metadata);
  • 链只存事实、不计算 dashboard 聚合。

该 pallet 在本仓库中已落地为 pallets/miner-registry/src/lib.rs,并以 pallet index13接入运行时(见 runtime/src/lib.rs 中pub type MinerRegistry = pallet_miner_registry;)。因为网络会以新 schema 重建(chain-wipe),所以无需历史数据迁移——这一点在源码中得到印证:runtime/src/lib.rs的版本注释显示该功能随 spec 106 加入后,后续版本均为增量演进。

3.2 候选 qblock id:通过 trait 边界解耦存储访问

注册 pallet 需要知道“当前候选 qblock id”,即矿工当前正在求解的目标编号。计划给出的公式是:

candidate_qblock_id = QuantumPow.latest_qblock_id().unwrap_or(0) + 1

为避免注册 pallet 硬编码对pallet-quantum-pow存储的直接访问,计划定义了一个小型 trait 边界:

pub trait QBlockIdProvider { fn latest_qblock_id() -> Option<u64>; fn candidate_qblock_id() -> u64 { Self::latest_qblock_id().unwrap_or(0).saturating_add(1) } }

该 trait 已在 pallets/miner-registry/src/lib.rs 中实现,语义与计划完全一致:

  • 空链(尚无任何 qblock)时latest_qblock_id()返回None,候选 id 即为1;
  • 已产出n个 qblock 时,候选 id 为n + 1(单调、1 起始的序数,与 Substrate 区块号相互独立)。

运行时接线:在 runtime/src/configs/mod.rs 中定义了RuntimeQBlockIds,它实现该 trait 并把调用转发给pallet_quantum_pow::Pallet::<Runtime>::latest_qblock_id();pallet_miner_registry::Config的type QBlockIds = RuntimeQBlockIds;完成注入。测试侧则在 pallets/miner-registry/src/mock.rs 用MockQBlockIds+set_latest_qblock_id()模拟不同的链进度。

而latest_qblock_id在 PoW pallet 中的真实实现(pallets/quantum-pow/src/lib.rs)为:

pub fn latest_qblock_id() -> Option<u64> { let count = QBlockCount::<T>::get(); (count > 0).then_some(count) }

即用 qblock 计数作为单调 id,保证“已产出数量 = 最新 id”。

3.3 存储设计:按账户存最新值,历史交给事件

计划对存储提出两个核心原则:

  1. 每个账户只存最新描述:NodeDescriptors<AccountId> -> NodeDescriptor;
  2. 每个账户只存最新参与记录,不按(qblock_id, account)存历史:LatestParticipation<AccountId> -> ParticipationRecord。

历史参与数据由类型化事件提供,存储条目只用于去重与最新状态查询,因此链状态按账户增长,而不是按(qblock_id, account)增长。

仓库实现完全遵循该原则,并在其上新增了反向索引(源码层面的锦上添花):

存储项类型用途
NodeDescriptorsStorageMap<AccountId, NodeDescriptor>每个账户的最新描述
LatestParticipationStorageMap<AccountId, ParticipationRecord>每个账户对当前候选 qblock的最新参与声明
ParticipantsByQBlockStorageDoubleMap<u64, AccountId, ParticipationRecord>每个 qblock 的全部参与者(反向索引,供枚举与分页)
ParticipantCountByQBlockStorageMap<u64, u32>每 qblock 参与人数,O(1) 计数

反向索引(见 pallets/miner-registry/src/lib.rs)解决了LatestParticipation无法回答“某 qblock 都有谁参与”的问题;由于参与天然是“每账户每 qblock 至多一次”,每个(qblock_id, account)至多写入一次,增长有界、与 PoW 的QBlocks存储同级。

3.4 类型化描述:用 Rust 类型替代不透明 JSON

计划要求:描述数据必须解码为类型化的运行时字段存储,而不是不透明的 JSON 字节。这样矿工无法提交无关数据,schema 迁移也是正式的(运行时升级可新增描述版本,继续接受旧版本或显式报错)。

建议的存储结构(已在源码中原样实现并扩展为NodeDescriptor):

pub struct NodeDescriptor<BoundedString, MinerSpecs, RpcEndpoints, BlockNumber> { pub schema_version: u16, pub node_id: BoundedString, pub node_name: BoundedString, pub public_host: Option<BoundedString>, pub public_port: Option<u16>, pub rpc_endpoints: RpcEndpoints, pub auto_mine: bool, pub log_level: LogLevel, pub miners: MinerSpecs, pub payload_hash: H256, pub updated_at: BlockNumber, }

实际存储版本(lib.rs的NodeDescriptor)在其基础上追加了deposit: Balance(保留的押金)、system_info: Option<SystemInfo>(硬件调查)与runtime: Option<RuntimeInfo>(节点软件身份),这是 schema v2 的产物(详见 3.8)。

字段长度约束(bounded strings):计划强调每个有界字符串都必须有合理上限,防止描述存储成为“无结构的大字节逃逸口”,并给出建议上限。运行时已在 runtime/src/configs/mod.rs 中把这些建议固化为实际常量:

node_id <= 64 bytes (MinerRegistryMaxNodeIdBytes) node_name <= 64 bytes (MinerRegistryMaxNodeNameBytes) public_host <= 253 bytes (MinerRegistryMaxPublicHostBytes) rpc_endpoint <= 256 bytes (MinerRegistryMaxRpcEndpointBytes) rpc_endpoints <= 8 条 (MinerRegistryMaxRpcEndpoints) miner label/name <= 64 bytes (MinerRegistryMaxMinerLabelBytes) miner backend/vendor <= 32 bytes (MinerRegistryMaxMinerBackendBytes) miner device id <= 64 bytes (MinerRegistryMaxMinerDeviceIdBytes,运行时取 128) miner specs <= 16 条 (MinerRegistryMaxMinerSpecs)

小型受控词表用枚举:计划建议对log_level、矿工种类、backend/vendor 等小规模受控词表优先用枚举而非有界字符串,只有操作员提供的标签、主机名、端点 URL、设备 id 才用有界字符串。源码实现了LogLevel(Debug/Info/Warning/Error)与MinerKind:

pub enum MinerKind { Cpu, Gpu, QpuDwave, QpuIbm, QpuIonq, QpuPasqal, Asic, Metal, // Apple Metal GPU backend,追加在末尾以保持既有变体 SCALE 索引稳定 }

Metal追加在末尾正是为了保证已上线链上既有变体的 SCALE variant 索引不变(源码注释明示)。

payload_hash 作为链下审计句柄:计划要求保留原始payload_hash,它可以是 quip-protocol 提交的规范描述字节的哈希,但链上必须校验并存储协议与索引器真正关心的类型化字段。set_descriptor的实现(见 3.6)正是对 SCALE 编码后的描述输入计算blake2_256作为payload_hash。

3.5 参与记录:MinerKind + 预算

计划的参与记录类型:

pub struct ParticipationRecord<BlockNumber> { pub qblock_id: u64, pub kind: MinerKind, pub budget_seconds: Option<u32>, pub updated_at: BlockNumber, }

源码实现与之一致:qblock_id必须是当前候选 id,kind为受控枚举,budget_seconds为可选的求解时长预算(如量子设备访问时长),updated_at记录提交区块号。

3.6 外置函数(Extrinsics):三个签名调用

计划新增三个签名调用:

set_descriptor(descriptor: NodeDescriptorInput) clear_descriptor() participate(qblock_id: u64, kind: MinerKind, budget_seconds: Option<u32>)

源码(pallets/miner-registry/src/lib.rs)以 call_index 0/1/2 逐一实现,校验规则与计划一一对应:

set_descriptor(call_index 0)

  • 校验schema_version是否为运行时支持的描述版本(通过版本化输入枚举保证);
  • 校验必填字段、有界字符串长度、有界列表、枚举值、端口范围、矿工清单结构;
  • 拒绝没有矿工规格(NoMiners)或缺少有用身份字段的描述;
  • 因NodeDescriptorInput只含已知字段,不支持的/无关数据在构造上就被拒绝;
  • 存储类型化描述,并以blake2_256(canonical_descriptor_bytes)作为payload_hash审计;
  • quip-protocol 侧的链下 JSON 校验仍可用于签名前提示操作员错误,但链上校验才是权威。

源码中的错误枚举(Error<T>)将上述规则形式化:EmptyNodeId、EmptyNodeName、EmptyPublicHost、EmptyRpcEndpoint、EmptyMinerLabel、EmptyMinerBackend、EmptyMinerDeviceId、NoMiners、InvalidPort、DescriptorNotFound、DescriptorRequired、InvalidQBlockId、DuplicateParticipation,以及 v2 引入的EmptyOsSystem、EmptyCpuBrand、EmptyCpuArch、EmptyGpuVendor、EmptyGpuName、InvalidGpuUtilization、EmptyPythonVersion、EmptyQuipVersion、EmptyDockerImage。

clear_descriptor(call_index 1)

  • 删除描述、返还押金,并连带删除该账户的参与记录(避免清除描述后存储中残留悬空行);
  • 无描述时返回DescriptorNotFound。

participate(call_index 2)

  • 要求qblock_id == candidate_qblock_id(即T::QBlockIds::candidate_qblock_id()),否则InvalidQBlockId;
  • 拒绝同一账户对同一 qblock 的重复参与(DuplicateParticipation),同时写入反向索引并递增ParticipantCountByQBlock;
  • 要求先有描述(DescriptorRequired)——启动流程本就强制提交描述,这能阻止匿名参与垃圾进入类型化事件流。测试 pallets/miner-registry/src/tests.rs 中participate_requires_descriptor、participate_rejects_stale_or_future_qblock_id、participate_rejects_duplicate_for_same_qblock分别验证这三条规则。

3.7 存款(Deposit):为描述存储保留余额

计划建议为描述存储保留余额,使用两个常量:

DescriptorDepositBase DescriptorDepositPerByte

更新描述时按新载荷大小调整保留额,清除时全额返还。运行时配置(runtime/src/configs/mod.rs):

MinerRegistryDescriptorDepositBase = MILLI_UNIT (10^9) MinerRegistryDescriptorDepositPerByte = MICRO_UNIT (10^6)

计算函数descriptor_deposit(payload_len) = DescriptorDepositBase + DescriptorDepositPerByte * payload_len。更新路径replace_descriptor采用差额结算:新押金大于旧押金则补 reserve 差额,小于则 unreserve 差额,并debug_assert!(remaining.is_zero())捕获账目漂移。v2 引入system_info_payload_len与runtime_payload_len,把变长字节计入押金(固定尺寸字段如核数、内存不计)。测试updating_descriptor_adjusts_reserved_deposit、clear_descriptor_removes_storage_and_unreserves_deposit、v2_deposit_exceeds_v1_by_system_info_and_runtime_bytes覆盖了调整、返还与 v2 增量计价。

3.8 描述 Schema 版本化:V1/V2 增量演进

计划建议用正式 Rust 类型定义描述 schema,避免把任意 JSON 载荷收进存储:

pub enum NodeDescriptorInput<...> { V1(NodeDescriptorV1Input<...>), }

set_descriptor匹配版本化输入、校验并转换为存储的NodeDescriptor;未来 schema 变更新增 V2 变体而非重载松散字节载荷,从而给运行时清晰的迁移路径,也给了 quip-protocol 精确的 SCALE 编码目标。

仓库已演进到V2(对应NODE_DESCRIPTOR_SCHEMA_V1 = 1、NODE_DESCRIPTOR_SCHEMA_V2 = 2,见lib.rs常量),实现要点:

  • 变体索引被固定:#[codec(index = 0)] V1恒编码为字节0x00、#[codec(index = 1)] V2恒为0x01,保证在途的 V1 签名外置函数不因源码中变体顺序变化而改变解码结果(测试node_descriptor_input_pins_variant_indices_and_round_trips断言v1.encode()[0] == 0、v2.encode()[0] == 1);
  • V2 = V1 + 可选system_info+ 可选runtime,字段顺序与 V1 相同,共享前缀编码方式一致;
  • system_info硬件调查:OsInfo(system/release/machine)、CpuInfo(logical_cores/physical_cores/brand/arch)、GpuInfo(index/vendor/name/memory_mb/utilization_pct 0..=100)、内存,全部变长字段为BoundedVec,使MaxEncodedLen有限;
  • runtime节点软件身份:python、quip_version(上限 48 字节)、protocol_version: u32、in_docker: bool、docker_image: Option<..>(上限 256 字节)。

选择类型化而非不透明 blob 的量化依据(见docs/miner-registry-system-info-v2-spec.md):实测同一份system_info载荷,类型化 SCALE 编码比 JSONBoundedVec<u8>约小 50%(如 1-GPU 笔记本 94 B vs 290 B,8×H100 极端场景 787 B vs 1462 B),且 polkadot.js 无需客户端解析器即可解码,还天然按字段限长。

存储迁移(v1 → v2):v2 给存储记录追加了尾部可选字段,改变了 SCALE 布局,因此 pallets/miner-registry/src/migrations.rs 提供MigrateToV2版本门控迁移:清空全部描述并把押金返还给原账户(unreserve,避免clear造成双重锁押),而不是做无损翻译——因为描述是自愈的,每个矿工重启时都会通过 startup auto-identify 重新提交。迁移被注册进运行时的Executivemigrations 元组(runtime/src/lib.rs 中的pallet_miner_registry::migrations::v2::MigrateToV2<Runtime>),pallet 存储版本升到 2。

3.9 事件:类型化、供索引器消费

计划定义三个类型化事件:

DescriptorUpdated { who, payload_hash, payload_len } DescriptorCleared { who } MinerParticipated { qblock_id, who, kind, budget_seconds }

源码 pallets/miner-registry/src/lib.rs 原样实现。设计要点:

  • 描述事件不携带完整载荷(载荷已在NodeDescriptors存储中),只带哈希与长度,事件与存储不重复;
  • 参与事件携带全部参与字段,使索引器无需立即做存储读取即可获得参与历史。

3.10 运行时 API:让 Python 客户端少依赖存储名

计划指出:存储查询对substrate-interface已足够,但运行时 API 能让 Python 客户端更少依赖存储名,建议提供:

node_descriptor(account) -> Option<NodeDescriptor> latest_participation(account) -> Option<ParticipationRecord>

这两个是便利 API,不应返回聚合统计。仓库实际演进在此基础上更进一步——MinerRegistryApi(runtime/src/apis.rs)目前提供面向索引器分页枚举的反向索引查询:

fn participants_by_qblock(qblock_id: u64, start_after: Option<AccountId>, limit: u32) -> Vec<(AccountId, ParticipationRecord<BlockNumber>)>; fn participant_count_by_qblock(qblock_id: u64) -> u32;

participants_by_qblock按账户排序以稳定分页,start_after作为游标只返回严格大于游标的账户,limit服务端上限 1,000;limit == 0返回空。这与计划“运行时只提供便利查询、不产聚合”的边界一致——计数来自维护好的存储,而非遍历。

围绕 qblock 身份,QuantumPowApi(api_version 2,见 pallets/quantum-pow/src/lib.rs 与 runtime/src/apis.rs)提供计划所需的全部查询:latest_qblock_id()、qblock_id_by_block(block_number)、qblock_by_id(qblock_id)、qblock_by_block(block_number)(qblock_by_block是winning_solution的 qblock 命名别名)、current_difficulty()/current_hardness()(实时难度,含衰减后的现值,与存储原始值不同)、difficulty_for(topology_hash)、mineable_topologies()、mining_snapshot()、topology_meta()。QuantumComputeMempoolApi则提供open_order_ids(start_after, limit)、job_order(order_id)、order_result(order_id)、order_top_solvers(order_id)——即计划中 mempool 发现所需的全部调用。

四、Python 客户端改造(quip-protocol 计划内容)

说明:quip-protocol是独立于本仓库的 Python 矿工客户端,以下为其改造计划;本仓库的 Rust 运行时侧已按该计划落地,可作为客户端实现的精确 SCALE 目标。

4.1 PoW qblock 查询包装

在quip-protocol/substrate/client.py增加包装:

query_latest_qblock_id() -> Optional[int] query_qblock_by_id(qblock_id: int) -> Optional[WinningSolutionWithNonce] query_qblock_by_block(block_number: int) -> Optional[WinningSolutionWithNonce] query_qblock_id_by_block(block_number: int) -> Optional[int] query_current_hardness() -> SubstrateDifficulty

并通过三条路径暴露:

  • quip-protocol/substrate/pool_client.py;
  • quip-protocol/substrate/pool.py的幂等操作清单;
  • pool/client 测试。

query_winning_solution_count()仅保留为临时兼容回退。这些包装直接对应 3.10 的QuantumPowApi运行时 API,服务端已就绪。

4.2 PoW solution number:latest_qblock_id + 1

用 qblock id 取代“对WinningSolutions计数 + 1”的逻辑:

solution_number = latest_qblock_id + 1 fresh chain = 1

主要改动点:

  • SubstrateMinerController._query_winning_solution_count_safe;
  • SubstrateMinerController._resolve_solution_number;
  • 仍把 solution number 描述为WinningSolutions键计数的注释与日志元数据。

提交证明成功后,继续按 Substrate 区块号校验,但优先使用 qblock 命名的运行时 API:

qblock_by_block(block_number) qblock_id_by_block(block_number)

并在本地提交日志中同时记录qblock_id与chain_block_number。这正是运行时端qblock_id_by_block/qblock_by_blockAPI 存在的直接动因:qblock id 是 1 起始的单调序数,与区块号相互独立,矿工不再需要重放事件来推断自己提交的是第几个 qblock。

4.3 Mempool 发现:状态优先(state-first)

新增包装:

query_open_order_ids(start_after: Optional[int] = None, limit: int = 100) -> list[int] query_order_result(order_id: int) query_order_top_solvers(order_id: int)

mempool 控制器改为状态优先的工作流:

  1. 启动时分页读取open_order_ids;
  2. 把每个 id 喂给既有的_consider_order(order_id)路径;
  3. 周期性重扫开放 id,或在区块唤醒时重扫并做节流(throttling);
  4. 保留JobProposed事件仅作低延迟快速路径;
  5. 保留OrderExpired事件仅作提示,状态读取始终是权威。

这样独立矿工在停机恢复后无需重放事件即可回到工作状态——对应QuantumComputeMempoolApi::open_order_ids的游标分页设计(start_after+limit)。

4.4 用类型化调用替换 remark

描述流:保留 Python 侧既有的描述构建器与校验器,把 remark 提交替换为:

set_node_descriptor(payload: bytes) query_node_descriptor(account: bytes)

需要更新的位置:

  • quip_cli.py中的启动自动识别(startup auto-identify);
  • quip-miner identify;
  • dry-run 行为(仍可打印规范 JSON 而不提交);
  • 重试/校验逻辑——改为从存储校验payload_hash,而不是等待 remark 事件。

参与流:用以下调用替换_mark_participating与_submit_participation_remark:

participate_qblock( qblock_id=solution_number, kind=miner_kind, budget_seconds=optional_budget, )

既有 worker 的"participating"消息可保留为内部触发器,但控制器应提交类型化运行时调用而非 remark。

接线完成后清理:

  • 删除quip-protocol/substrate/remark.py;
  • 删除System.remark_with_event回退测试;
  • 删除quip_cli.py与substrate/miner_controller.py中 remark 专属注释;
  • 更新quip-miner.example.toml中提及描述 remark 的注释。

五、索引器契约(Indexer Contract)

计划为索引器划定明确的数据来源,全部是类型化链上状态与事件,不再解析 remark:

  • NodeDescriptors存储:每个账户的最新节点描述;
  • DescriptorUpdated/DescriptorCleared事件:何时刷新描述缓存;
  • MinerParticipated事件:参与历史(索引器据此构建历史视图,链上LatestParticipation只存最新值,二者互补,参见 3.3);
  • qblock 运行时 API 或存储:qblock 身份与获胜载荷(qblock_by_id/qblock_by_block/latest_qblock_id等);
  • mempool open-order API 或存储:当前任务发现(open_order_ids/job_order)。

边界:索引器继续自行计算聚合统计。运行时不添加计数器、图表、参与率、舰队汇总或 dashboard 专用 rollup。这一边界在运行时端是“按构造”成立的——MinerRegistryApi只暴露分页枚举与计数,没有聚合函数。

六、实施顺序(Implementation Order)

计划给出的十步实施顺序如下,可作为任何实现该方案的团队的执行清单:

  1. 新增pallet-miner-registry,含描述存储与类型化事件;
  2. 新增参与存储、校验与类型化事件;
  3. 把 pallet 接入runtime,提升spec_version,添加测试;
  4. 为 qblock、open-order、描述、参与 API 增加 Python client/pool 包装;
  5. 把 PoW solution number 切换到latest_qblock_id + 1;
  6. 把 mempool 发现切换到open_order_ids+job_order;
  7. 用set_descriptor替换 auto-identify 与quip-miner identify的 remark;
  8. 用participate替换参与 remark;
  9. 删除substrate/remark.py与 remark 专属测试;
  10. 更新矿工、索引器与面向仪表盘的文档。

对照仓库现状:步骤 1、2、3 已由pallet-miner-registry及其运行时接线完成(pallet index 13,spec_version已推进至 118,transaction_version7,详见 runtime/src/lib.rs 的版本注释历史),运行时 API(步骤 4 的服务端部分)也已就绪。

七、测试计划(Test Plan)

7.1 运行时测试

计划列出的运行时测试点,仓库中已逐一实现于 pallets/miner-registry/src/tests.rs:

  • 描述设置:set_descriptor_stores_typed_descriptor_and_reserves_deposit——存储载荷、payload_hash、区块号并保留押金(断言reserved_balance(1) == stored.deposit);
  • 描述更新调整押金:updating_descriptor_adjusts_reserved_deposit——缩小描述后保留额随之下降;
  • 描述清除:clear_descriptor_removes_storage_and_unreserves_deposit——删除存储、返还押金并发出DescriptorCleared;
  • 超长/空描述被拒:set_descriptor_rejects_empty_required_fields、set_descriptor_rejects_empty_optional_strings_when_present、set_descriptor_rejects_descriptor_without_miners、system_info_bounds_reject_over_length_input(含对Decode边界绕过攻击的防护验证:Vec<u8>与BoundedVec编码紧凑长度前缀相同,超长原始字节必须被解码拒绝);
  • 当前候选 qblock 参与成功:participate_records_current_candidate_qblock(set_latest_qblock_id(Some(4))后参与 id 5);
  • 同账户同 qblock 重复参与被拒:participate_rejects_duplicate_for_same_qblock;
  • 过期/未来 qblock id 被拒:participate_rejects_stale_or_future_qblock_id;
  • 获胜后可参与下一个候选 id:participate_rejects_duplicate_for_same_qblock后半段(推进latest_qblock_id后参与 id 2 成功);
  • 描述要求被执行:participate_requires_descriptor;
  • 反向索引:participants_by_qblock_lists_all_sorted_with_pagination(排序、limit上限、start_after游标、limit 0返回空)、participants_are_indexed_per_qblock(跨 qblock 保留历史,LatestParticipation只留最新);
  • V2 描述:set_descriptor_v2_stores_and_returns_system_info、set_descriptor_v2_without_system_info_stores_none、set_descriptor_v2_rejects_empty_required_system_info_fields、set_descriptor_v2_rejects_gpu_utilization_over_100、set_descriptor_v2_stores_empty_gpu_list、set_descriptor_v2_allows_empty_os_release_and_machine、set_descriptor_v2_stores_and_returns_runtime、set_descriptor_v2_without_runtime_stores_none、set_descriptor_v2_stores_runtime_without_docker_image、v2_deposit_exceeds_v1_by_system_info_and_runtime_bytes。

7.2 Python 测试(quip-protocol)

计划列出的客户端测试点:

  • pool/client 包装正确编码并路由新的运行时 API 调用;
  • solution number 解析使用latest_qblock_id + 1;
  • 新链上 solution number 解析为1;
  • 校验后在提交日志记录 qblock id;
  • mempool 控制器从open_order_ids播种工作;
  • 基于事件的JobProposed仍是快速路径但不是唯一路径;
  • identify 提交set_descriptor并校验存储哈希;
  • 参与提交类型化运行时调用,不再构造 System remark。

八、总结

把矿工可观测数据从System.remark迁移到pallet-miner-registry,本质是一次从“无类型约定”到“类型化链公民”的架构收敛:矿工获得独立于索引器的恢复能力,索引器获得单一类型化数据源且不再解析不透明 JSON,仪表盘退化为纯索引器消费者,而链只承诺“存储原始事实、绝不产聚合”。本仓库的 Rust 侧已完整落地该方案——包括QBlockIdProvidertrait 解耦、V1/V2 版本化描述、押金定价、反向参与索引与配套运行时 API——仓库内的测试与迁移代码(pallets/miner-registry/src/tests.rs、pallets/miner-registry/src/migrations.rs)为任何后续实现(尤其是 quip-protocol Python 客户端侧)提供了可直接对标的精确行为契约。

【免费下载链接】quip-validator

A rust implementation of the Quip Protocol forked from Substrate

项目地址:https://gitcode.com/gh_mirrors/qu/quip-validator
点击查看免费下载
上一篇:GetQzonehistory:QQ空间历史说说与图片的完整本地备份指南
下一篇:generative-ai-for-beginners 云环境搭建指南:使用 GitHub Codespaces 零安装跑通全部 21 节课

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询