ARTICLE DETAIL

资讯详情

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

fhevm Listener 开发规范解读:如何构建零事件丢失的区块链监听器

fhevm Listener 开发规范解读:如何构建零事件丢失的区块链监听器 fhevm Listener 开发规范解读如何构建零事件丢失的区块链监听器【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevmfhevm 仓库中的 listener 是一个面向 EVM 链的零事件丢失zero-event-loss区块链监听器它并行轮询 RPC 节点、验证区块哈希链以检测重组reorg并把规范链上的区块、交易、回执发布到 Redis Streams 或 RabbitMQ 供下游消费。而 listener/docs/guidelines.md 正是这个模块的开发纲领——一份规定了什么代码可以进、什么代码不可以进的工程规范。本文以该文档为核心骨架逐条解读其背后的设计动机并结合 listener 的实际源码cursor 游标、reorg 回溯、broker 抽象、指标埋点验证这些规范如何在生产级代码中落地。读完本文你将理解一套面向绝不能丢数据场景的 Rust 服务端开发纪律。一、规范的核心GIVE NO ROOM TO MISS SOMETHINGlistener/docs/guidelines.md 开篇即点明了全部规范的第一性原理The rationale is: GIVE NO ROOM TO MISS SOMETHING (a block, a transaction, a receipt, a log), crash or skip a processing is not permitted.监听器是链上事件的唯一数据入口一旦漏掉一个区块、一笔交易、一条回执或一条日志下游索引器、relayer、coprocessor 都会得到不完整的数据视图且很难事后弥补。因此 listener 的容错模型不是尽量不丢而是结构性杜绝漏的可能任何一步失败要么无限重试要么显式死信唯独不允许悄悄跳过。这一原则在 listener/README.md 中被凝练成三条核心保证没有任何区块、交易或回执会被跳过——即使进程崩溃重组简单重组、来回重组、多分支重组都会被检测并处理瞬时性的 RPC/broker 故障在熔断器保护下无限重试。下文所有规范条目都可以还原为对这三条保证的支撑。二、规范逐条解读从原则到源码落地1. MUST NEVER panic绝不 panic运行时路径上不允许 panic。在 listener/crates/listener_core/src/core/evm_listener.rs 中可以清晰看到这条纪律的两面运行时 panic 被当作严重 bug。fetch_blocks_and_run_cursor中通过tokio::join!等待 producer并行拉块与 consumer顺序校验入账两个任务对JoinHandle结果的JoinError处理是这样的let cursor_outcome cursor_join_result.map_err(|join_err| { cancel_token.cancel(); error!(error %join_err, Cursor task panicked — this is a critical bug); EvmListenerError::InvariantViolation { message: format!(Cursor task panicked: {}, join_err), } })?;代码注释直白地写着a JoinError means the task panicked, which is a critical bug (we never panic in our code)——任务 panic 被建模为不变量违例InvariantViolation属于永久性错误会被上报而非静默吞掉。初始化失败可以 panic且是故意为之。唯一的例外是validate_strategy_and_init_block如果 RPC 不可达、策略与节点不兼容、数据库不可达或起始区块无法获取直接panic!。注释给出了理由初始化失败时进程无法正确工作应当立即崩溃触发重启crash-loop-backoff 模式让编排系统拉起一个新的、干净的实例。这是启动期 fail-fast运行期零 panic的分层设计。2. No uncontrolled unwraps禁止不受控制的 unwrap规范没有一刀切禁用unwrap而是强调受控。在 listener 中几乎所有的Result都被显式映射到统一的错误类型EvmListenerError见 evm_listener.rs 顶部的#[derive(Error, Debug)]枚举例如#[error(Could not fetch block: {source})] CouldNotFetchBlock { source: BlockFetchError }, #[error(Database error: {source})] DatabaseError { source: SqlError }, #[error(Invariant violation: {message})] InvariantViolation { message: String },任何一步失败都以?向上传播并在调用边界被统一分类详见第四节而不是在源头unwrap()一把梭。unwrap_or这类有默认值的受控解包允许存在例如batch_receipts_size_range.unwrap_or(1)因为它不会让程序崩溃。3. Retry indefinitely raise an alert无限重试 必要时告警大多数时候遇到错误应该无限重试并在需要关注时发出告警——这是规范中最关键的一条。listener 的 broker 抽象把它实现为错误分类 熔断器两层机制详见 listener/crates/shared/broker/README.md瞬时错误transient基础设施故障数据库、RPC、broker 抖动消息本身没问题获得无限重试预算且连续失败会触发熔断器暂停消费防止在故障期间污染死信队列永久错误permanent消息载荷本身非法反序列化失败、校验失败重试永远不可能成功直接计入max_retries后进死信dead-letter。对应的处理语义在 listener/crates/listener_core/src/core/workers.rs 的classify函数中体现EvmListenerError的 9 个变体被显式、穷尽无通配符地划分为两类——基础设施类全部映射为HandlerError::transient而InvariantViolation单独映射为HandlerError::permanent。注释强调no wildcard, so that adding a new EvmListenerError variant forces a conscious classification decision at compile time新增一种错误必须显式表态它是瞬时还是永久编译期就堵住漏分类的可能。4. Be consistent in error management错误管理保持一致规范要求错误管理风格统一anyhow 或 box dyn即无论用anyhow::Error还是Boxdyn Error全项目保持一致。listener 的选择是自定义强类型错误枚举EvmListenerError使用thiserror派生每个变体带#[source]保留底层错误链broker 层则统一通过HandlerError::transient/permanent两个构造器出口。这意味着从 RPC 层、存储层到 broker 消费循环错误类型一路一致、分类口径一致、指标标签一致error_kind_label把每个错误变体映射为静态标签如block_fetch、database、invariant_violation见 listener/crates/listener_core/src/metrics.rs任何一层出错都能在监控上以同一套维度观测。5. Think alerting考虑告警永远要考虑告警在 listener 中直接落地为一套完整的 Prometheus 指标体系集中在 listener/crates/listener_core/src/metrics.rs。与零事件丢失强相关的关键指标包括指标类型含义listener_cursor_iterations_totalcounter主 cursor 循环迭代次数停滞检测速率应恒大于 0listener_reorgs_totalcounter检测到的链重组次数listener_db_tip_block_number/listener_chain_height_block_numbergauge数据库已入账的最新区块号 vs RPC 报告的最新区块号listener_transient_errors_total/listener_permanent_errors_totalcounter瞬时 / 永久错误的分类计数listener_compute_block_failure_total等counter区块哈希 / 交易根 / 回执根校验失败次数值得注意的两个细节体现了告警必须第一时间可观测init_gauges/init_counters在启动时把 gauge 和 counter预置为 0注释解释了原因increase()/rate()需要窗口内至少两个采样点才能算出增量如果计数器从不存在直接跳到 1Grafana 面板会误报为 0。预置 0 值让第一次真实故障立即显示为 1指标全部带chain_id标签多链部署时可按链独立告警一条链的停滞不会掩盖另一条链的异常。6. Think profiling考虑性能剖析规范要求必要时考虑 profiling。listener 用三类指标支撑性能剖析listener_block_fetch_duration_seconds、listener_range_fetch_duration_seconds等 histogram 记录单块拉取与整段范围的墙钟耗时listener_rpc_request_duration_seconds、listener_rpc_semaphore_available观测 RPC 并发信号量的水位同时通过block_fetcher策略切换见下一条对不同拉取模式做基准对比。这些数据可直接支撑哪个阶段是瓶颈的判断——是 RPC 时延、回执批大小还是入账事务耗时。7. Think strategy pattern考虑策略模式规范的策略模式要求在 listener 中最典型地体现在区块拉取策略上。配置结构BlockFetcherStrategy见 listener/crates/listener_core/src/config/config.rs枚举了五种可互换的策略block_receipts区块 该区块内所有回执一次拉取batch_receipts_full区块 全量回执批量拉取batch_receipts_range区块 按批大小并行批量拉回执transaction_receipts_parallel区块 逐笔交易回执并行拉取transaction_receipts_sequential区块 逐笔交易回执串行拉取。消费端get_block_by_number/get_block_by_hash根据当前配置分发到对应方法见 evm_listener.rs上层调用方完全无感。这使运维可以在不改代码的前提下针对不同 RPC 节点的能力是否支持批量 JSON-RPC、回执批次上限等选择最合适的策略并在初始化时通过validate_strategy_and_init_block实际拉一次最新区块来验证策略与节点兼容不兼容直接拒绝启动。8. Think separations of concerns关注点分离listener 的架构严格贯彻了关注点分离从 crate 划分到模块划分层层递进crate 层listener_core主程序拉块、校验、入账、发布、broker后端无关的消息代理抽象、primitives共享类型与路由常量见 listener/README.md 的 Crates 表模块层blockchain/RPC 与区块计算、core/cursor、过滤器、发布器、slot buffer、worker handler、store/PostgreSQL 模型与仓储、config/、metrics.rs见 listener/crates/listener_core/src 目录结构流程层每个业务流live cursor、finality、reorg、catchup、cleaner各自由独立的Handler承担且通过 PostgreSQL advisory lock 实现互斥见 listener/crates/listener_core/src/core/workers.rs。例如在workers.rs中可以看到 8 个 handler 各司其职FetchHandler跑 live cursor、FinalityHandler跑最终性流、ReorgHandler处理重组回溯、CatchupHandler负责把大范围补块请求切分成子范围、CleanerHandler定期清理。注释明确写了每个 handler 获取哪种 advisory lockFetchHandler与ReorgHandler共享同一把锁保证同一链上 fetch 与 reorg 永不并行这是关注点分离 并发安全结合的范例。9. Think reusable code考虑可复用代码最典型的复用是broker crate 的一处实现、双后端可用。listener/crates/shared/broker/README.md 说明同一套BrokerAPI 既可以Broker::redis(redis://...)连接 Redis Streams也可以Broker::amqp(amqp://...)连接 RabbitMQTopic是后端无关的路由标识namespace.routing运行时分别映射为 Redis 的流名/死信流名和 AMQP 的路由键/队列名。发布、消费、重试、死信、熔断的语义完全一致应用代码零改动只有基础设施接线不同。消费模式direct routing、fanout、competing consumers和熔断器状态机Closed → Open → Half-Open也在 broker 层统一实现被 listener_core 及仓库内其他模块复用。三、规范在关键流程中的体现cursor 与 reorg 的崩溃安全1. 生产者-消费者游标乱序拉取、顺序入账fetch_blocks_and_run_cursorevm_listener.rs是 live 流程的主入口其设计本身就是零丢失的体现读取数据库中最新的规范区块DB tip向 RPC 查询当前链高计算出待拉区间[db_tip1, min(chain_height, db_tiprange_size)]通过tokio::spawn同时启动producerfetch_blocks_in_parallel并行 RPC 乱序填充AsyncSlotBuffer和consumercursor_processing按槽位顺序校验parent_hash链并入库tokio::join!等待两者完成保证一个失败不会弃置另一个。重排检测是显式结果而非错误CursorResult::ReorgDetected携带block_number/block_hash/parent_hash与Complete、UpToDate并列——注释专门强调Reorgs are a normal operational event on blockchains (not an error)。检测到重排后不 sleep立即把ReorgBacktrackEvent发布到BACKTRACK_REORG路由交给ReorgHandler处理。2. 重组回溯先发布、后提交、崩溃即重来reorg_backtrack的算法注释完整描述了它的崩溃安全设计是crash or skip is not permitted最硬核的落地Phase 1只读从重排点 N 开始用parent_hash逐块向前回溯沿途边拉边发布事件BlockFlow::Reorged仅保留轻量元数据每个区块约 72 字节的NewDatabaseBlock全程不写数据库Phase 2单事务把收集到的区块反转为升序通过batch_upsert_blocks_canonical在一个数据库事务里批量入账失败则整体回滚数据库保持原状Phase 3恢复发布FETCH_NEW_BLOCKS让 cursor 恢复。其崩溃语义被显式列出Phase 1/2 崩溃 → 数据库未变重试从头回溯、重复发布at-least-once下游按(block_number, block_hash)去重Phase 2 提交后崩溃 → 数据库已正确重试只需回溯约 1 个区块。任何崩溃点都不会导致部分入账 部分未发布的中间态——这正是不跳过任何处理的结构性保证。3. 分布式互斥advisory lock 防止重复处理放大多个 handler 在 workers.rs 中都遵循同一套模式处理前try_acquirePostgreSQL advisory lock若锁被其他 pod 持有则直接 Ack 而不是 requeue避免重复投递导致循环自我放大处理完成后先释放锁再发布下一条触发消息消除与其它 handler 的竞态。CleanerHandler的注释点明了动机如果没有这把锁HPA 扩容出的多个副本加上消息重投会让自延续的清理循环成倍扩散。这把锁把无限重试与重复处理这对矛盾在分布式层面化解了。四、规范如何约束配置默认值里的工程纪律规范最终要落到可运行的配置上。listener/config/listener-default.yaml 是全部默认值的唯一真源其中的关键默认值与上述规范一一对应blockchain: finality_depth: 64 # 未定案窗口深度 finality_tag: true # 优先使用节点的 finalized 标签 finality_active: true # 启用最终性流fetch-final-block strategy: automatic_startup: true block_start_on_first_start: current # 首次启动从链高-1开始重排安全 range_size: 100 # 单次 cursor 批次大小 loop_delay_ms: 1000 # 批次间退避 max_parallel_requests: 50 # RPC 并发上限 block_fetcher: block_receipts batch_receipts_size_range: 10 compute_block: false # 是否校验区块哈希/交易根/回执根 compute_block_allow_skipping: true max_exponential_backoff_ms: 20000 # RPC 指数退避上限 catchup: prefetch: 5 claim_min_idle_secs: 3600 catchup_max_sub_range: 100要点解读block_start_on_first_start: current在源码中被解析为链高 - 1config.rs 与 evm_listener.rs 中saturating_sub(1)为的就是首个区块的重排安全max_exponential_backoff_ms配合 RPC 拉取的指数退避create_fetcher传入体现无限重试但要有礼貌compute_block: false默认关闭区块完整性校验追求吞吐但打开后compute_block_allow_skipping允许在特定失败下继续且所有校验失败都有独立计数器listener_compute_block_failure_total可供告警——关闭的校验不代表放弃观测finality_tag: true时最终块取节点的finalized标签否则取head - finality_depth两种模式都有对应 gaugelistener_final_height_block_number暴露。finality_depth: 64、RPC 超时与并发信号量等配置共同构成了既不能漏块、又不能压垮 RPC的平衡面。五、给二次开发者如何遵守这份规范如果你要在 listener 上扩展功能listener/docs/guidelines.md 可以直接转译成一份可执行的 checklist新增错误变体时必须同时更新EvmListenerError并给出classify的显式分类分支瞬时 vs 永久以及error_kind_label的静态标签——三处缺一不可新增流程时优先实现broker::Handler并在 workers.rs 中注册遵循先取 advisory lock → 处理 → 先释放锁再发布下一条 → Ack的既有模式新增指标时在 metrics.rs 中同时补describe_metrics的 HELP 文本与init_gauges/init_counters的预置 0 值保证首次故障即可观测新增配置项时在 listener/config/listener-default.yaml 中补齐默认值该文件被 Helm chart 作为基础层加载是配置默认值的唯一真源任何跳过某条数据的念头都应被GIVE NO ROOM TO MISS SOMETHING否决——要么重试、要么死信、要么在崩溃后从数据库已提交状态重放绝无第三条路。结语listener/docs/guidelines.md 全文不足二十行却浓缩了一套生产级区块链索引器的全部工程哲学把不丢数据从口号变成可执行的编译期约束穷尽式错误分类、运行期机制无限重试 熔断 advisory lock 互斥、崩溃安全协议先发布后提交的回溯与可观测性兜底预置 0 值的指标体系。任何一行代码的取舍都能在这份规范里找到出处——这正是小而精的工程文档应有的样子原则极少但一旦违背代价极大。【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表