指南 · 第 7 部分,共 8 部分
在 CI 中锚定你的发布
你交付的每一个发布都是一组字节:tarball、wheel、容器镜像、一段提交区间。为这些字节锚定一份 Label 309 存在性证明(PoE),会把「相信我们的话,这个构建就是我们发布的东西」变成「这里有一笔为它作证的 Cardano 交易」。你把每个产物计算哈希得到一个叶子,把这些叶子折叠成一个 Merkle 根,然后在链上以元数据标签 309 发布这个根。从此,任何持有该交易引用的人都能证明某个产物在其区块时间或更早之前就已存在,仅凭公链即可,无需账号,也无需信任你的流水线或你的供应商。
产物的字节永远不会离开 runner。CLI 在本地计算哈希,只发布摘要,因此这对私有仓库和闭源构建都是安全的:上链的是一个定长哈希,绝不是你的代码。
核心工具:cardanowall attest
本页的一切都通过与网关无关的 cardanowall CLI 的一条命令完成(crates.io 上的 crate cardanowall-cli,并在 label-309-cli 发布页 提供预编译二进制文件)。attest 是为 CI 量身打造的入口:它对你的输入计算哈希,通过网关取价并发布一条记录,然后等待你所要求的生命周期状态。
用一个基础 URL 和一个发布范围的 API 密钥把它指向任意 Label 309 网关,再交给它一些要计算哈希的东西:
export CARDANOWALL_API_KEY="…" # a publish-scoped key from your gateway
cardanowall attest \
--paths 'dist/*' \
--base-url https://your-gateway.example/api/v1 \
--wait confirmed \
--receipt-out poe-receipt.json--base-url 和 --api-key 也会从 CARDANOWALL_BASE_URL 和 CARDANOWALL_API_KEY 读取,因此在 CI 中,只要这些值由你的密钥库设置,二者便可从命令中省去。
选择锚定内容的三种方式
恰好设置一个输入;模式由它决定。
文件。 --paths 接受一个字面路径或一个 glob 模式,可重复。每个叶子是某个文件字节的 SHA-256。选集会去重,并按规范化的相对路径逐字节排序,因此同一个工作树总会产出同一个根,与 shell 展开 glob 的顺序无关。给 glob 加上引号,以免你的 shell 先行展开它:
cardanowall attest --paths 'dist/**/*.tar.gz' --paths 'dist/**/*.whl'提交。 --commits 接受一个 git rev-list 区间;每个叶子是某个原始提交对象的 SHA-256,从旧到新。这会锚定历史本身的来源。它需要 runner 上有完整的 git 历史,因为浅克隆无法解析该区间:
cardanowall attest --commits v1.0.0..v1.1.0预先计算的摘要。 --leaf 接受一个你在别处算好的 64 位十六进制摘要,可重复,并按参数顺序保留。用它来锚定 CLI 从不以文件形式看到的东西,比如一个 OCI 镜像摘要:
cardanowall attest --leaf 9f86d0818840…0a08 # a 64-hex digest, e.g. an image digest单个叶子发布一条单项记录;多个叶子则发布一条 Merkle 记录,其根被锚定在链上,同时叶子列表被上传,使每一项日后都能获得一份包含证书。
清单与回执
在文件模式下,attest 会在其输出旁写出一个确定性的 poe-manifest.json(用 --manifest-out 改名)。清单记录了它所锚定的每个文件的「名称到哈希」绑定,且相同的输入总会产出逐字节相同的清单字节。加上 --anchor-manifest,可将清单的 SHA-256 作为最后一个叶子折叠进去,这样文件名与哈希之间的绑定本身也成为根所承诺的一部分。
--receipt-out 会写出一份带版本的 JSON 回执,其中载有记录、报价、交易以及等待快照。把它作为你这次构建的凭据留存:它就是日后 verify 用来找到并核对锚点所需的全部内容。把它存为工作流工件、附到发布上,或与 changelog 一起提交。
等待、待处理与重复运行
默认情况下,attest 会等待交易越过确认阈值(--wait confirmed);--wait submitted 则在交易一到达网络便返回。等待有一个期限(--timeout,默认 600 秒)。若期限届满,输出与回执仍会被写出,进程以 3(待处理)退出:发布不会丢失,它会在网关上继续,你稍后可凭回执重新查看。当报价超过你的上限时,--max-usd 上限会拒绝发布(退出码 1,在任何上传之前),因此价格骤涨绝不会给某条流水线带来意外账单。
重复运行在设计上就是安全的。attest 默认不发送幂等性头;网关转而对逐字节相同的记录去重,因此重复运行同一个构建绝不会再锚定第二次,也绝不会重复计费。再次锚定同一个 dist/,第二次运行会免费重放第一条记录。
GitHub Actions
cardanowall/poe-attest 这个 action 为 GitHub 工作流封装了同一个 CLI。它是开源的,并在供应链层面被固定:它内嵌了所运行 CLI 发布版的 SHA-256 摘要,并在每次运行前同时校验下载的归档与解出的二进制文件,因此被调包的发布产物无法蒙混过关。
在仓库上保存两个机密:GATEWAY_URL(你网关数据平面的基础 URL,以 /api/v1 结尾)与 GATEWAY_API_KEY(一个发布范围的密钥),然后在发布时锚定你的发布产物:
name: anchor-release
on:
release:
types: [published]
permissions:
contents: read
jobs:
attest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: cardanowall/poe-attest@v1
with:
gateway-url: ${{ secrets.GATEWAY_URL }}
api-key: ${{ secrets.GATEWAY_API_KEY }}
paths: |
dist/**/*.tar.gz
dist/**/*.whl该步骤写出回执,打印一份带交易和验证链接的摘要卡片,并暴露若干输出(tx、record-id、verify-url 等)供后续步骤使用。
用专用的 CI 身份为数据流签名
要让一条流水线的锚点可归属、可发现,就用一个身份种子为每条记录签名。网关随即会按签名者的公钥为记录建立索引,于是任何人都能用 ?signer=<public-key> 从网关的记录源中列出一条流水线的全部历史。签名始终是可选的,验证方从不要求它。
为每条流水线使用一个专用的、一次性的身份,绝不使用个人种子。把它存为受环境保护的机密,使得只有来自该环境的运行才能读取它:
jobs:
attest:
runs-on: ubuntu-latest
environment: release-signing
steps:
- uses: actions/checkout@v7
- uses: cardanowall/poe-attest@v1
with:
gateway-url: ${{ secrets.GATEWAY_URL }}
api-key: ${{ secrets.GATEWAY_API_KEY }}
seed: ${{ secrets.CI_SIGNING_SEED }}
paths: dist/**/*.tar.gz种子会在日志中被掩码,且仅通过 stdin 传给 CLI,绝不出现在命令行上;任何机密都绝不会到达网关:计算哈希与签名都在本地进行,只有记录和公开数据会被发布。
切勿从 pull_request_target 锚定
该触发器会把你仓库的机密暴露给来自派生(fork)拉取请求的代码。只从你能掌控的事件锚定,例如
release、push 或 workflow_dispatch。上面这个最小 job 只需要 contents: read;只有当你还要把回执附到发布上时,才加上 contents: write。
该 action 支持的输入比这里展示的更多,包括每个叶子一份包含证书、把产物附到发布上、价格上限,以及超时策略。完整清单见 action 的 README。
GitLab CI/CD
在 GitLab 上,同一个包装器以 CI/CD 组件的形式提供。job 直接在 CLI 自己的容器镜像里运行(按版本和摘要固定),运行时无需安装任何东西——而且和本页其他地方一样,任何 Label 309 网关都可以用,无论是托管的还是自建的:
include:
- component: gitlab.com/cardanowall/poe-attest/attest@1
inputs:
gateway-url: https://your-gateway.example/api/v1
paths: |
dist/**/*.tar.gz
dist/**/*.whl在 Settings → CI/CD → Variables 里把机密添加为 CI/CD 变量,并勾选 masked 和 protected:CARDANOWALL_API_KEY(发布权限的密钥),以及仅当你要签名时的 CARDANOWALL_SEED——务必设在项目本身上,绝不要设在群组上:继承会让每个子项目的锚定都被同一个身份悄悄签名。默认情况下 job 只在受保护标签的流水线里运行,未受保护的 ref 永远花不掉你的余额;要在其他事件上锚定,请覆盖输入 rules。
结果以 dotenv 报告的形式返回:任何通过 needs: 引用锚定 job 的下游 job,都能直接读到交易哈希、验证链接,以及 18 个 POE_* 变量中的其余部分:
announce:
needs: [poe-attest]
script:
- echo "anchored in $POE_TX"
- echo "verify at $POE_VERIFY_URL"该组件支持的输入比这里展示的更多,包括包含证书、价格上限、runner 标签,以及超时策略。完整清单见组件的 README。
其他 CI 系统
同一个 CLI 到处都能跑。使用容器镜像 ghcr.io/cardanowall/label-309-cli(其 entrypoint 为 cardanowall),或从发布页取一个预编译二进制文件。
任何其他 runner,只要二进制文件在 PATH 上:
export CARDANOWALL_BASE_URL="https://your-gateway.example/api/v1"
export CARDANOWALL_API_KEY="$YOUR_CI_SECRET"
cardanowall attest \
--paths 'dist/*' \
--wait confirmed \
--receipt-out poe-receipt.json
# exit 0 = reached the wait target; 3 = pending (publish continues on the gateway);
# 1 = refused (for example over --max-usd) or failed.你需要网关提供什么
发布会把一笔交易放到 Cardano 上,而这需要一笔手续费,所以 attest 需要一个网关来提交。任何 Label 309 网关都可以:一个托管运营方,或你自己的自托管网关(开源的 label-309-gateway,一个 Rust 二进制文件加 Postgres)。从 CI 出发,你只需要它的两样东西:一个数据平面的基础 URL,以及一个发布范围(poe:create)、由预付余额支撑的 API 密钥。
网关拥有那只已注资的 Cardano 钱包,并从自己的余额模型中支付手续费。你的 CI 不持有任何钱包密钥,也不持有链上资金。一个泄露的 API 密钥最多只能花掉该账户的预付余额去做更多锚定;它无法转移资金、读取你的内容,也无法冒你之名签名。你可以随时轮换或吊销它。
验证锚点
这种锚定之所以有价值,恰恰是因为任何人都能在没有你的情况下核对它。凭回执中的交易引用,验证会独立地针对公链和你所选的浏览器运行,无需账号,也无需网关:
cardanowall verify <tx-hash>它解析交易,对记录做结构校验,核对任何签名,确认记录已定案,并以退出码的形式返回一个结论,因此它接入下游检查时,与 attest 契合发布一侧一样干净。要把某个产物与其锚点核对,就对文件计算哈希再比对;对于 Merkle 记录,则构建一份包含证书,把一个产物钉到已发布的根上。完整的验证方模型见验证。
证明比流水线更长寿
一个 Label 309 锚点是标签 309 之下的普通元数据,而不是供应商回执。在 runner 早已消失、镜像仓库几经轮换、CI 系统只剩记忆之后很久,那笔交易依然为你的产物在其区块时间就已存在作证。任何人都能从公链验证它,无需账号,也无需信任发布它的人。