Union Drip:构建支持多链多资产的 Cosmos 水龙头(GraphQL + 批量 MultiSend 实现解析)
Drip 是 Union 仓库中实现 Cosmos 生态水龙头(Faucet)的独立 Rust 服务,同时支持多条链、每条链上多个 denom 的测试币发放,也是 Union 官方水龙头前端(app.union.build/faucet)的后端。本文基于 drip/README.md 的用法与 drip/src/main.rs 的源码实现,完整覆盖其启动命令、配置文件字段、GraphQL 请求协议、批量转账工作线程与 Cloudflare Turnstile 人机校验的实现细节,读完后可独立部署一个自己的多链 Faucet 并理解其防滥用与批量出块优化的内部机制。
1. 定位:一个“多链、多 denom”的 Cosmos Faucet
drip/README.md 对 Drip 的官方描述只有一句话:
Faucet for Cosmos chains. Supports multiple chains and multiple denoms per chains.
结合 drip/src/main.rs 的实现可以看到,Drip 的设计要点是:
- 多链:配置文件
chains是数组,每条链一个独立的工作线程轮询自己的数据库队列、连接自己的节点(见第 5 节poll_loop); - 多 denom:每条链的
coins也是数组,允许同一链上发放多种代币; - 异步队列:HTTP/GraphQL 请求只负责把发放请求写入 SQLite,真正广播交易由后台 worker 批量完成,从而把 N 笔转账合并进一笔
MsgMultiSend交易,显著降低 gas 成本。
2. 快速开始:四终端演示(README 原始步骤)
README 给出的完整演示从仓库根目录执行,共四个 Tab(以下命令原样继承自 drip/README.md):
Tab 1,启动 Union Devnet:
nix run .#devnet-union -L
Tab 2,启动 Stargaze Devnet(可选,用于演示多链):
nix run .#devnet-stargaze -L
Tab 3,启动 Drip 服务:
nix run .#drip -- -c ./drip/config.json
Tab 4,发起发放请求:
cat ./drip/example-requests/union-devnet.json | http POST localhost:8000
cat ./drip/example-requests/stargaze-devnet.json | http POST localhost:8000
两点需要说明:
- 服务固定监听
0.0.0.0:8000(见 main.rs 的TcpListener::bind("0.0.0.0:8000")),GET /打开 GraphiQL 交互界面,POST /接收 GraphQL 请求。 - 示例请求文件实际上就是一份 GraphQL mutation 文档。以 drip/example-requests/union-devnet.json 为例:
{
"query": "mutation UnoFaucetMutation($chain_id: String!, $denom: String!, $address: String!, $captchaToken: String!) { send(chainId: $chain_id, denom: $denom, address: $address, captchaToken: $captchaToken) }",
"variables": {
"chain_id": "union-devnet-1",
"denom": "muno",
"address": "union1m87a5scxnnk83wfwapxlufzm58qe2v65985exff70z95a2yr86yq7hl08h",
"captchaToken": "helloworld"
}
}
即通过 send(captchaToken, chainId, address, denom) 这个 mutation 申请一次发放,返回值为交易哈希字符串(失败时返回 "ERROR" 或 "ERROR: ratelimited")。注意示例中的 denom: "muno" 是旧值——当前 drip/config.json 中 Union Devnet 链配置的 denom 已是 au(仓库提交历史中存在 chore(devnet): muno -> au 的变更),而 send mutation 会校验 denom 必须存在于该链的 coins 列表中,因此实际请求时应与 config 中的 denom 保持一致,否则会得到 invalid denom 错误。
3. 命令行参数
drip/src/main.rs 使用 clap 定义了三个参数:
| 参数 | 短选项 | 默认值 | 说明 |
|---|---|---|---|
--config-file-path |
-c |
必填 | 配置文件路径(JSON),README 中即为 ./drip/config.json |
--batch-size |
-b |
6000 |
单个链 worker 每轮从队列中取出并合并进一笔交易的最大请求数 |
--max-paginated-responses |
-m |
50 |
各查询接口的单次最大返回条数上限 |
配置加载逻辑在 main.rs:读取文件后 serde_json::from_str::<Config> 解析,文件不存在或 JSON 非法时直接 panic 退出。
4. 配置文件全字段说明
配置结构由 main.rs 中的 Config、Chain、Coin、GasFillerConfig 等类型定义,仓库内的 drip/config.json 是双链示例。逐字段说明如下:
4.1 顶层 Config
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
chains |
Vec<Chain> |
必填 | 支持发放的链列表 |
log_format |
text / json |
text |
日志格式,分别对应 tracing 的文本/JSON 输出(见 main.rs) |
secret |
Option<String> |
无(可选) | Cloudflare Turnstile 的站点密钥,配置后所有请求必须携带有效的人机校验 token |
bypass_secret |
Option<String> |
无(可选) | 后门绕过密钥:captchaToken 等于该值时跳过 Turnstile 校验,示例配置中为 "helloworld",对应示例请求里的 "captchaToken": "helloworld" |
max_request_polls |
u32 |
必填(示例为 7) | send mutation 等待交易哈希的最多轮询次数,每轮间隔 1 秒,超过后返回 "ERROR" |
ratelimit_seconds |
u32 |
0 |
同一 (chain_id, denom, address) 三元组两次请求的最小间隔秒数,低于该间隔返回 "ERROR: ratelimited" |
4.2 Chain 条目
示例配置中 Union Devnet 条目(节选自 drip/config.json):
{
"id": "union-devnet-1",
"bech32_prefix": "union",
"memo": "drip drop greetings from union faucet",
"ws_url": "ws://localhost:26657/websocket",
"grpc_url": "http://localhost:9090",
"gas_config": { "gas_price": "1.0", "gas_denom": "au", "gas_multiplier": "1.1", "max_gas": 40000000 },
"signer": "0xaa820fa947beb242032a41b6dc9a8b9c37d8f5fbcda0966b1ec80335b10a7d6f",
"coins": [ { "denom": "au", "amount": 13370 } ]
}
各字段含义:
id:链的唯一标识,必须与目标节点的chain-id完全一致。ChainClient::new连接后会assert_eq!(chain_id, chain.id)强制校验(见 main.rs),不一致直接 panic;bech32_prefix:如union、stars,用于校验请求地址的 HRP 前缀(sendmutation 中会 bech32 解码并比对);memo:广播交易时附带的 memo 字符串;- 节点连接地址:示例配置中为
ws_url(CometBFT 节点 WebSocket RPC 端口 26657/26757)与grpc_url(gRPC 端口 9090/9190)。需要留意的是,当前快照中 main.rs 的Chain结构体字段名为rpc_url,与示例配置中的ws_url/grpc_url字段名并不一致,说明该仓库快照处于字段命名演进过程中;实际部署时应以当时可编译通过的代码结构为准,保证配置键与结构体字段匹配; signer:发放账户的私钥(32 字节 hex)。私钥只存在服务端配置中,客户端只提交地址;coins:该链可发放的 denom 与每次发放数量数组,支持同链多 denom;gas_config:标签式枚举(tag = "type"),源码支持三种 gas 填充策略(见 main.rs):Fixed(fixed::GasFiller):固定 gas price,示例配置中gas_price/gas_denom/gas_multiplier/max_gas属于这类固定价格配置;Feemarket(FeemarketConfig):字段max_gas、gas_multiplier(可选)、denom(可选),查询链上 base fee 后按倍数加价;OsmosisEip1559Feemarket(OsmosisEip1559FeemarketConfig):在 Feemarket 基础上增加base_fee_multiplier,适配 Osmosis 的 EIP-1559 式费用市场。
4.3 运行时的数据库
Drip 启动时在进程工作目录创建 SQLite 数据库 db.sqlite3(WAL 模式),并建表 requests(见 main.rs):
CREATE TABLE IF NOT EXISTS requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id TEXT NOT NULL,
denom TEXT NOT NULL,
address TEXT NOT NULL,
time TEXT,
tx_hash TEXT
)
这是整个“请求-发放-回执”解耦的中枢:GraphQL 层写入 tx_hash IS NULL 的待处理行,worker 消费后回填交易哈希(失败时回填 ERROR: ... 文本)。
5. 请求链路:send mutation 的完整校验
send 实现于 main.rs,校验与处理顺序为:
- 链存在性:
chain_id必须命中chains中的id,否则invalid chain_id; - denom 存在性:denom 必须在该链
coins中,否则invalid denom; - 人机校验:配置了
secret时,调用 drip/src/turnstile.rs 的verify()向 Cloudflare Turnstile 的siteverify端点提交 token 与 secret 做服务端验证;若 token 恰好等于bypass_secret(CaptchaBypassSecret)则直接放行,这就是示例请求能用"helloworld"通过的原因; - 地址前缀校验:对
address做 bech32 解码,HRP 必须等于该链bech32_prefix; - 限流:查询该
(chain_id, denom, address)最近一次请求时间,距今不足ratelimit_seconds秒则直接返回"ERROR: ratelimited"; - 入队:
INSERT ... RETURNING id写入请求(time取datetime('now')); - 轮询回执:每秒查询该行的
tx_hash,最多max_request_polls次,拿到后返回哈希字符串,超时返回"ERROR"。
注意该接口是“同步等待 + 异步发放”的混合模式:客户端最长阻塞约 max_request_polls 秒。
6. 发放 worker:批量 MsgMultiSend 的核心实现
每条链启动一个 poll_loop 监督任务,内部不断 spawn 工作线程(见 main.rs),每轮循环:
- 取出该链待处理请求:
SELECT id, denom, address FROM requests WHERE tx_hash IS NULL AND chain_id IS ?1 LIMIT ?2,?2即--batch-size; - 队列空则睡 1 秒重试;
- 调用
ChainClient::send(&requests)广播,失败最多重试 5 次(attempt递增记录日志); - 成功后用 SQLite 的
arrayvtab 模块执行UPDATE requests SET tx_hash = ?1 WHERE id IN rarray(?2),把同一批请求一次性回填为同一个交易哈希(交易哈希按 Cosmos SDK 惯例大写化后入库)。
send 方法(main.rs)是 gas 优化的关键:
- 先按 denom 聚合(
aggregate_by_denom),得到每种 denom 的总数量; - 构造一笔
bank/v1beta1.MsgMultiSend:inputs只有 1 个(faucet 账户,携带各 denom 的总量),outputs为每个请求一个; - 通过
broadcast_tx_commit同步等待上链并记录gas_used。
也就是说,同一批窗口内的 100 个地址、2 个 denom,最终只消耗 1 笔交易的 gas,这是 Drip 相比“每请求一交易”的朴素实现最显著的成本优势。
工作线程还做了容错设计:ChainClient 在每次 worker 重建时重新创建(注释说明为让 keyring 在 panic 后可重建),监督循环捕获 panic 后等 1 秒重启,保证单链故障不会拖垮进程。
7. 查询接口:转账历史
Query 对象提供三个分页查询(main.rs),均支持 limit(默认 10,上限为 --max-paginated-responses)与 offset_time 游标(默认当前时间,按 time < offset 倒序翻页):
| 查询 | 含义 |
|---|---|
handled_transfers |
已成功发放的记录(tx_hash IS NOT NULL 且不以 ERROR 开头) |
unhandled_transfers |
仍在排队中的记录(tx_hash IS NULL) |
transfers_for_address |
某地址的全部历史请求 |
返回结构为 Request { id, address, time, tx_hash },tx_hash 为可选(排队中为空)。这些查询是水龙头前端展示“发放中/已发放”状态的数据来源。
8. 部署:Nix 包与 NixOS 服务模块
drip/drip.nix 提供了两层部署能力:
- Nix 包:
crane.buildWorkspaceMember "drip"构建二进制,即 README 中nix run .#drip -- -c ./drip/config.json的入口; - NixOS 模块:开启
services.drip.enable后生成 systemd 服务drip,配置项为package、config(attrs,会被序列化成 JSON 写入临时文件并以-c传入)、log-level(映射到RUST_LOG环境变量,服务Restart = always)。
依赖方面,drip/Cargo.toml 显示 Drip 复用了 Union 工作区内的 cosmos-client(节点连接、gas 填充、本地签名钱包)、protos(bank/auth 的 protobuf 定义)、async-sqlite(启用 array/vtab 特性以支持 rarray 批量更新)与 async-graphql-axum。
9. 关键文件索引
| 内容 | 路径 |
|---|---|
| 用法文档 | drip/README.md |
| 双链示例配置 | drip/config.json |
| 服务主程序(GraphQL、worker、批量发送) | drip/src/main.rs |
| Turnstile 校验 | drip/src/turnstile.rs |
| 示例请求(Union / Stargaze) | drip/example-requests/union-devnet.json、drip/example-requests/stargaze-devnet.json |
| Nix 包与 NixOS 模块 | drip/drip.nix |
总结来说,Drip 用“GraphQL 入队 + SQLite 队列 + 每链批量 MsgMultiSend worker + Turnstile/限流双重防滥用”的架构,实现了一个可水平扩展到任意多条 Cosmos 链、且发放成本与请求数近似无关的水龙头服务;理解其 batch-size、max_request_polls、ratelimit_seconds 与三种 gas 填充配置,即可将其改造为任何 Cosmos 系项目的测试币发放基础设施。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00