ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

fhevm Relayer 标识符体系深度解析:六类 ID 的设计、映射与全链路追踪

2026/9/12 16:05:48 拓冰建站 浏览量
fhevm Relayer 标识符体系深度解析:六类 ID 的设计、映射与全链路追踪 fhevm Relayer 标识符体系深度解析六类 ID 的设计、映射与全链路追踪【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm导读fhevm 的全同态加密FHE链上操作——如 public-decrypt公开解密、user-decrypt用户解密与 input-proof输入证明校验——都是典型的异步长流程HTTP 请求到达 relayer 后要经历数据库落盘、编排器派发、网关链合约执行、链上事件监听、结果索引等多个阶段最终由客户端轮询获取结果。要让这条跨客户端、API 网关、relayer 进程、数据库与区块链的多跳链路可追踪、可去重、可恢复就必须为每个环节定义清晰且互不混淆的标识符。本文基于 relayer/docs/ID.md 并结合 relayer 的 Rust 源码系统梳理 fhevm relayer 的六类标识符内部请求 ID、外部请求 ID、内部解密索引 ID、外部引用 ID、网关解密 ID 与网关输入证明 ID讲解它们的格式选型动机、创建时机、存储位置、日志行为与 1:1 映射关系帮助读者在二次开发、运维排障与协议对接时快速定位问题。一、为什么 relayer 需要一整套标识符体系fhevm relayer 的 v2 API 采用提交即返回 轮询取结果的异步模型。以POST /v2/public-decrypt为例一次完整的端到端流程跨越以下环节客户端提交解密请求经过 Cloudflare 与 Kong 等入口代理到达 relayerrelayer HTTP handler解析并校验请求体计算内容哈希内部 ID以冲突插入的方式去重后写入数据库并立即返回202 Accepted与一个job_id编排器Orchestrator消费事件驱动请求进入网关链交易发送流程网关链上的合约如 Decryption Manager / Input Proof 合约成功受理后在交易回执中产生一个数字形式的decryptionId/ input proof idrelayer 的事件监听模块从链上日志中解析出该 ID将其映射回数据库中的内部 ID索引结果客户端用第一步拿到的job_id轮询GET /v2/.../{job_id}直至拿到成功结果或终态错误。在这个流程中同一条业务请求在不同的子系统里有不同的身份对外部代理与客户端而言需要随机不可预测的 ID防枚举、防撞库对 relayer 内部而言需要可排序、可快速定位的 ID对去重逻辑而言需要基于内容的确定性 ID而对网关链合约而言则是一个与 relayer 无关的数值 ID。六类标识符正是分别针对这些诉求设计的。二、六类标识符全景relayer 共使用六类标识符覆盖内部请求 / 外部请求 / 解密索引 / 外部引用 / 网关解密 / 网关输入证明六个维度标识符用途格式创建方用户可见是否入库Internal Request ID内部请求 ID在 relayer 内部追踪一次 HTTP 请求的异步处理过程UUID v7时间可排序relayer HTTP handler每次 HTTP 请求v2 POST /、v2 GET /否仅 input-proof 场景存入 Requests DBExternal Request ID外部请求 ID跨客户端、Cloudflare、Kong 与 relayer 追踪请求UUID v4完全随机、不可预测Kong 插件每次 HTTP 请求是通过响应头X-Request-ID返回否Internal Public/User Decryption Indexer ID内部解密索引 ID基于请求体内容对 public/user-decryption 请求去重内容哈希XXH3当前实现为 SHA-256确定性relayer HTTP handler请求首次出现时v2 POST /user-decrypt、POST /public-decrypt否是External Reference ID外部引用 ID客户端用于轮询 v2 API 结果的引用标识UUID v4完全随机、不可预测relayer HTTP handler是随响应返回并用于 GET 轮询是Indexer DBGateway Public/User Decryption ID网关解密 ID在网关链上关联用户/公开解密请求与响应数字Numberscheme 对 relayer 不透明网关链上的 Decryption Manager 合约解密请求成功时否可能出现在响应中是Indexer DBGateway Input Proof ID网关输入证明 ID在网关链上关联输入证明请求与响应数字Numberscheme 对 relayer 不透明网关链上的 Input Proof 合约请求成功时否可能出现在响应中是Indexer DB需要说明的是原文档描述的格式与当前仓库源码存在两处细节出入本文后续会结合源码逐一点明以源码为准。三、Internal Request IDrelayer 内部的请求追踪锚点设计目的与格式选型Internal Request ID用于在 relayer 内部追踪一次 HTTP 请求的异步处理过程。由于 relayer 的 HTTP handler 与编排器、事件监听模块运行在同一进程内日志是排障的主要手段因此该 ID 需要满足两个核心诉求强唯一性高并发下每个请求都必须有独立 ID避免日志串扰时间可排序性ID 的字典序应近似反映创建时间顺序便于在日志中按时间窗口检索、回放请求序列。基于此文档规定其格式为UUID v7——UUID v7 的时间前缀部分使其天然可排序随机部分保证了同一时间戳内的唯一性。这一点在 ids.rs 中有直接对应实现/// Generates unique, time-ordered request IDs that are safe to use concurrently. pub fn new_internal_request_id() - Uuid { Uuid::now_v7() }该模块的单元测试专门验证了 UUID v7 的顺序正确性test_sequential_ids_sort_correctly与test_delayed_ids_sort_correctly后者在两次生成之间sleep(2ms)断言生成的 ID 字符串序列与排序结果一致证明其时间可排序特性。同时test_concurrent_uniqueness用 100 个 tokio 任务各生成 100 个 ID验证 10 000 个 ID 全部唯一。创建时机与使用范围文档规定其创建时机为HTTP handler 在每次 HTTP 请求v2 POST、v2 GET时创建。从源码看各 v2 handler 在进入处理函数时即生成一个每次请求独立的request_id并贯穿于该请求的全部日志中例如 input_proof.rslet request_id Uuid::new_v4(); let _span span!(Level::INFO, handle-input-proof-post-req, request_id %request_id); info!( step %InputProofStep::ReqReceived, req_id %request_id, Handling input proof v2 POST request );值得注意的细节是当前 handler 实现中每个 HTTP 请求的request_id实际由Uuid::new_v4()生成而文档规定 Internal Request ID 使用 UUID v7。两者都是强唯一 ID差异仅在于可排序性。此外input_proof.rs 中还有一个独立的request_id_for_response同样由Uuid::new_v4()生成它在响应返回前重新生成用于标识本次 HTTP 响应而不是这次业务请求——这意味着一个 input-proof POST 请求的日志可能同时出现两个 ID处理期request_id与响应期request_id_for_response排障时需要注意区分。存储与可见性用户可见性否该 ID 仅存在于 relayer 内部日志与 span 上下文中。存储文档规定它仅在 input-proof 场景下存入 Requests DB从仓储层看input_proof_repo.rs 负责 input-proof 请求的落库而解密类请求则分别由 public_decrypt_repo.rs 与 user_decrypt_repo.rs 管理。日志relayer 在每一条与某个 HTTP 请求对应的日志中都会携带该 ID。四、External Request ID跨客户端与代理网关的链路追踪设计目的与格式选型External Request ID用于在客户端 → Cloudflare → Kong → relayer整条链路上追踪同一次 HTTP 请求。由于它要穿越多个独立系统其格式选型为UUID v4完全随机、不可预测opaque避免了客户端或中间代理通过有序 ID 猜测请求规模同时具备强唯一性。创建与传递路径按文档定义该 ID 由Kong 插件在每次 HTTP 请求到达时创建并贯穿以下日志链路Kong创建 ID 并写入请求/响应日志Relayer在 HTTP handler 入口处与 Internal Request ID 一起记录一次Relayer-SDK在收到响应时记录。用户侧则通过每个 HTTP 响应的X-Request-ID响应头拿到该 ID。从 responses.rs 的AppResponse结构定义看relayer 的响应体同样携带request_id字段v2 各类型响应error.rs、public_decrypt.rs 等都内嵌了该字段便于客户端把业务失败与具体请求对应起来。存储与可见性用户可见性是客户端在每次 HTTP 响应的X-Request-ID头中接收。存储不落库它只服务于瞬时链路的日志关联没有持久化价值。注意原文档在Relationship一节对该 ID 与其他 ID 的映射关系标注为???说明这一映射在当前设计中尚未明确约定结合源码可以推断它更接近一种全链路追踪上下文类比分布式追踪的 trace id而非与业务 ID 建立严格 1:1 绑定。五、Internal Public/User Decryption Indexer ID基于内容的去重键设计目的为什么解密请求需要内容级去重用户/公开解密请求具有幂等性特征客户端在网络抖动后重试、或多个客户端提交了相同内容的解密请求业务上都应只执行一次。如果以随机 ID 作为去重键同样的请求会被重复派发到网关链产生重复交易与多余开销。因此 relayer 对 public/user-decryption 请求采用基于请求体内容哈希的去重键即 Internal Public/User Decryption Indexer ID——payload 相同则 ID 相同天然实现去重。哈希计算规则哪些字段参与文档规定该 ID 格式为XXH3 哈希确定性并给出了两条关键规则对于public-decrypt对完整 payload计算哈希对于user-decrypt对将request_validity字段置空后的 payload计算哈希。理由是该字段仅用于约束 relayer 必须在有效期前将请求转发至网关链一旦请求已被成功处理并索引查询结果时可以直接忽略该字段因此它不应参与去重否则同一业务请求因有效期不同会被视为不同请求。从源码看当前实现实际采用SHA-256sha2crate并抽象为统一的ContentHashertrait见 ids.rspub trait ContentHasher { /// Computes a deterministic SHA-256 hash of the implementing types content. fn content_hash(self) - [u8; 32]; }哈希结果被转换为 32 字节的JobId见 job_id.rs作为事件路由与请求去重的统一键文档中的内部解密索引 ID在实现层即JobId。各请求类型的哈希字段如下PublicDecryptRequestpublic_decrypt_request.rs——完整 payload 参与hasher.update(bct_handles:); // 1 for handle in self.ct_handles { hasher.update(handle); } hasher.update(bextra_data:); // 2 hasher.update(self.extra_data);UserDecryptRequestuser_decrypt_request.rs——分三种 attestation 格式处理且明确排除signature与request_validityLegacyDirect按contract_addresses→contracts_chain_id→ct_handle_contract_pairs→extra_data→public_key→user_address顺序哈希LegacyDelegated在直接委托基础上追加delegator_address→delegate_addressEip712UnifiedV1先写入variant:eip712_unified_v1:前缀再哈希allowed_contracts→handles→extra_data→public_key→user_address。源码注释指出排除signature与有效期字段是因为它们在获得解密 ID 之前已在链上被消费不应参与去重门槛而variant:eip712_unified_v1:前缀则确保统一 EIP-712 格式与 legacy 格式即使 handle 集合相同也不会产生哈希碰撞这一点由测试attestation_formats_hash_differently验证。字段名的显式标签如bcontract_addresses:与固定顺序共同保证了不同实例间哈希的确定性。InputProofRequestinput_proof_request.rs——虽然 input-proof 的内部去重键文档未单独列为一类但实现上同样采用内容哈希contract_chain_id→contract_address→user_address→ciphertext_with_zk_proof→extra_data且测试test_input_proof_request_each_field_affects_hash逐个验证每个字段都会影响哈希。创建时机与去重落地文档规定该 ID 由 HTTP handler 在请求首次出现时创建v2 POST /user-decrypt、POST /public-decrypt。源码中的落地方式非常典型handler 先计算content_hash得到int_job_id再调用仓储层的冲突插入方法如 public_decrypt.rslet int_job_id: JobId request.content_hash().into(); // ... let insert_outcome self .public_decrypt_repo .insert_data_on_conflict_and_get_ext_job_id(proposed_ext_job_id, int_job_id.as_ref(), request.clone(), dispatch_epoch) .await; // Inserted / DuplicateCompleted / DuplicateProcessing 三种结果统一取出已分配或新分配的 ext_job_idInserted表示首次出现随后才向编排器派发事件DuplicateCompleted/DuplicateProcessing表示命中去重直接复用已有记录的外部引用 ID不再重复派发。这正是内容哈希去重在实现层的完整闭环同一个内部 ID 永远映射到同一个外部引用 ID重复提交只是查询。存储、日志与可见性可见性用户不可见内部键。存储入库作为 Indexer DB 中记录的主键/唯一键。日志relayer 在索引indexing相关阶段的日志中携带该 ID与事件监听阶段的日志区分开。六、External Reference ID客户端轮询的 job_id设计目的External Reference ID是 v2 API 中暴露给客户端的业务引用标识即响应体里的job_id。它有两个用途POST 响应中返回GET 轮询时作为路径参数使用。格式选型同样为UUID v4对客户端完全透明、不可预测避免客户端从 ID 推断系统内部信息。创建时机区分两种场景按文档规定External Reference ID 的创建有两种模式解密类请求user/public-decrypt只在 payload 首次出现时创建随后续相同请求从数据库中按内部解密索引 ID 取出——这正是上一节去重逻辑的直接结果input-proof 请求每次请求都创建不过 input-proof 同样有基于int_job_id的去重查询见 input_proof.rs只是新请求与重复请求都会在响应中给出一个ext_job_id。源码中由 ids.rs 的new_external_reference_id()统一生成/// Generates random external reference IDs for client-facing operations. pub fn new_external_reference_id() - Uuid { Uuid::new_v4() }该函数配套的测试覆盖了唯一性含并发、随机性排序后与生成序不同以及 UUID 版本位test_external_reference_version断言版本位为 4与 RFC 4122 variant 位test_external_reference_variant从侧面验证了完全随机这一设计意图。与内部 ID 的 1:1 映射对 user/public-decryption与Internal Public/User Decryption Indexer ID1:1 映射对 input-proof与Internal Request ID内部任务 ID1:1 映射。这条映射是去重与轮询两条链路共同依赖的枢纽关系写入侧由insert_data_on_conflict_and_get_ext_job_id保证同一内部 ID 不会产生第二个外部引用 ID读取侧由find_status_by_ext_idinput-proof与find_status_and_res_by_ext_idpublic-decrypt等仓储方法按外部 ID 查回内部记录。可见性与存储可见性是。POST 响应v2 POST /user-decrypt、POST /public-decrypt、POST /input-proof中返回job_idGET 轮询v2 GET /user-decrypt/{job_id}、GET /public-decrypt/{job_id}、GET /input-proof/{job_id}中作为路径参数使用。存储存入 Indexer DB。日志relayer 在创建索引条目时记录一次并在 GET 请求的 HTTP handler 中再记录一次。七、Gateway Public/User Decryption ID 与 Gateway Input Proof ID链上映射键为什么需要对 relayer 不透明的数字 ID这两类 ID 由网关链上的合约产生用于在链上把解密/输入证明请求与响应关联起来。其格式为数字Number且文档明确说明其 scheme 对 relayer不透明——relayer 无需理解该数字的编码规则只需原样保存并在事件关联时使用。这体现了 relayer 与网关链合约之间的边界ID 的语义只由签发它的合约定义。产生时机与映射关系Gateway Public/User Decryption ID由网关链上的Decryption Manager 合约在解密请求成功受理时产生与Internal Public/User Decryption Indexer ID1:1 映射Gateway Input Proof ID由网关链上的Input Proof 合约在输入证明请求成功受理时产生与Internal Request ID1:1 映射。源码中relayer 的网关交互模块从交易回执与链上日志中提取这两个数字 ID。例如 user_decrypt_handler.rs 从响应中读取decryptionId并以gw_reference_id为日志字段贯穿后续事件监听流程直到映射回内部索引 IDpublic_decrypt_handler.rs 同样从回执中提取decryptionId并调用complete_req_with_res完成结果关联。链上日志侧user_decrypt_handler.rs 展示了从事件 topic 中解析出 32 字节的decryption_id_topic并转换为U256数字 ID 的过程。可见性与存储可见性用户默认不可见但文档注明可能出现在响应中——即 relayer 未来可以将链上 ID 作为附加信息返回给客户端存储存入 Indexer DB日志relayer 在事件监听阶段尚未映射回内部 ID 之前的日志中以该 ID 为关联键映射完成后切换为内部 ID。这一点对排障非常重要同一条请求的日志中网关侧阶段用数字 ID 标识索引侧阶段用内容哈希标识二者通过 DB 记录衔接。八、映射关系全景与一次请求的 ID 生命周期将以上六类 ID 的 1:1 映射串联起来可以画出两条典型的ID 链解密类请求public/user-decryptExternal Request ID (Kong, X-Request-ID, 不落库) │ 入口日志关联 Internal Public/User Decryption Indexer ID (内容哈希, 落库) ──1:1──► External Reference ID (UUID v4, 落库, 暴露给客户端) │ Gateway Public/User Decryption ID (合约数字ID, 落库, 事件监听阶段日志)输入证明请求input-proofExternal Request ID (Kong, X-Request-ID, 不落库) │ 入口日志关联 Internal Request ID (任务ID, 落库) ──1:1──► External Reference ID (UUID v4, 落库, 暴露给客户端) │ Gateway Input Proof ID (合约数字ID, 落库, 事件监听阶段日志)以一次POST /v2/public-decrypt为例完整的 ID 生命周期为Kong 生成External Request ID并注入X-Request-IDrelayer 入口处与本次 handler 的Internal Request ID一起记入日志handler 对完整 payload 计算Internal Decryption Indexer ID内容哈希首次出现时生成External Reference ID并落库响应202 Accepted返回job_id与Retry-After编排器派发至网关链合约受理后回执携带Gateway Decryption ID事件监听模块以网关数字 ID 为键处理事件映射回内部索引 ID 后完成结果索引客户端携带job_id轮询 GEThandler 按External Reference ID查库返回最终结果。这一链条贯穿了 relayer/src/http/endpoints/v2、relayer/src/orchestrator、relayer/src/gateway 与 relayer/src/store/sql 四个目录每一跳都有对应的日志字段与 DB 列。九、排障与可观测性实践如何用 ID 串联日志基于上文的设计日常排障可以遵循以下ID 换挡经验请求刚进来时用X-Request-IDExternal Request ID在 Kong/Cloudflare 与 relayer 入口日志之间关联确认请求是否到达 relayer业务处理期用 handler 日志中的request_idInternal Request ID检索 relayer 侧完整处理过程包括解析、去重命中DedupHit、入队Queued、派发等步骤去重判断日志中的int_job_id内容哈希可以跨多个客户端请求对比——相同int_job_id说明它们是同一业务请求的重复提交这解释了为什么响应会返回相同的ext_job_id网关链阶段日志切换到gw_reference_id数字 ID需要与链上交易回执、事件日志对照确认合约是否成功受理结果查询客户端通过ext_job_idExternal Reference ID轮询 GET 接口对照数据库状态机Queued → Processing → TxInFlight → ReceiptReceived → Completed / TimedOut / Failure见 req_status_enum_model.rs判断当前进度。此外relayer 的 v2 GET 接口在请求仍处于处理中时返回202并附带动态计算的Retry-After源码见 input_proof.rs 与 public_decrypt.rs客户端应遵循该头进行指数退避轮询而不是固定频率重试。十、文档与实现的差异说明本文依据的 relayer/docs/ID.md 是 relayer 的标识符设计文档阅读时需注意两处与当前实现不一致的地方均以源码为准哈希算法文档写 Internal Public/User Decryption Indexer ID 使用XXH3哈希而当前源码的ContentHasher实现public_decrypt_request.rs、user_decrypt_request.rs、input_proof_request.rs统一使用SHA-256结果封装为 32 字节JobId内部请求 ID 版本文档规定 Internal Request ID 为 UUID v7而当前各 v2 handler 每次请求生成的request_id实际使用Uuid::new_v4()ids.rs 中确实保留了Uuid::now_v7()风格的内部 ID 生成函数及其排序性测试但 v2 HTTP 层的即时日志 ID 目前为 v4。理解这些差异有助于在阅读日志时准确对照文档与代码设计文档描述的是应当如此而排障时应以实际日志中出现的 ID 形态为准。结语fhevm relayer 的六类标识符并非冗余而是对异步、跨系统、可去重、可恢复这一架构诉求的精确回应UUID v7/v4 分别解决内部排序与外部保密内容哈希解决幂等去重链上数字 ID 则充当 relayer 与网关合约之间的翻译键。理解这套 ID 体系就等于拿到了阅读 relayer 日志、理解数据库表结构与排查端到端解密/证明流程的完整地图。后续阅读可继续深入 relayer/src 下的 orchestrator、gateway 与 store 模块或参考 relayer/docs 中的其他设计文档如 status-transitions.md、queries.md以补全状态机与数据模型视角。【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考