可重复的容器构建

由于其共识协议,Internet Computer 始终正确运行容器的代码。 但这并不意味着它正在运行容器的_correct_ 代码。 如果您使用的是其他人开发的容器,您可能需要验证容器是否确实在运行某些预期代码,然后才能控制它为您做出重要决定,例如,将您的 ICP 发送到容器智能合约。 验证这一点需要回答两个问题:

  1. 正在为容器执行哪个 WebAssembly (Wasm) 代码?

  2. 这些容器通常是用更高级的语言编写的,例如 Motoko 或 Rust,而不是直接用 Wasm 编写。 第二个问题是:正在运行的 Wasm 真的是编译声称的源代码的结果吗?

文档的其余部分回答了这些问题。 对于第一个问题,我们将了解 Internet Computer 如何提供有关容器代码的信息。 为了能够回答第二个问题,容器作者应确保可信且 reproducible builds 来自源代码的 Wasm 代码。 这样的构建允许任何人遵循与容器作者相同的步骤并产生完全相同的 Wasm 代码, 然后可以将其与 Internet Computer 上的容器中执行的 Wasm 代码进行比较。

熟悉可重现构建主题的不耐烦的读者可以直接跳到summary

找出 Internet Computer 正在执行的容器代码

Internet Computer 不允许您访问任意容器的 Wasm 代码。 这是一个设计决策,因为开发人员可能希望将某些代码保密。 但是,Internet Computer 确实允许您访问容器 Wasm 代码的 SHA-256。

要获取此哈希,您必须首先记下要检查其代码的 Internet Computer 容器的principal。 例如,假设我们对 Internet Identity 容器的代码感兴趣,其principal是“rdmx6-jaaaa-aaaaa-aaadq-cai”。 然后,访问此服务的最简单方法是从终端使用 dfx 工具。 打开你的终端,然后运行:

$ dfx canister --network ic info rdmx6-jaaaa-aaaaa-aaadq-cai
Controller: r7inp-6aaaa-aaaaa-aaabq-cai
Module hash: 0xda8d1bdd93fbf1edddeb2a423fa3c528c2e4877d39beed2277a12e60227e44d4
如果您正在运行旧版本的“dfx”,则需要从包含有效“dfx.json”文件的目录中运行此命令。 如果您没有这样的目录,您可以使用 dfx new 创建它。

在这里,Internet Computer 告诉我们“rdmx6-jaaaa-aaaaa-aaadq-cai”容器(恰好是 Internet 身份容器)的 Wasm 模块的哈希是“0xda8d1bdd93fbf1edddeb2a423fa3c528c2e4877d39beed2277a12e60227e44d4”。

上面的检查为您提供了容器 Wasm 模块的 current 哈希值,但 Internet Computer 容器的 controllers 可能随时更改代码(例如,升级容器)。 但是,如果控制器列表为空,则您知道容器是不可变的,因为没有人有权更改代码。

有了这个散列,您接下来可以检查它是否对应于某些给定的源代码。 这仅在代码的构建过程是可重现的情况下才有效。

可重现的构建

作为容器作者,您必须向用户提供一些东西以允许他们重现您的构建:

  • 用于为容器创建 Wasm 模块的源代码相同。

  • 有关如何重新创建构建环境的说明。

  • 有关如何从源代码重复构建 Wasm 的过程的说明。 至关重要的是,该过程必须是确定性的,以确保它产生完全相同的 Wasm。 它还必须是可信的,这样用户才能确信 Wasm 是源代码的忠实翻译,而不是恶意构建工具的产物。

接下来,我们将更详细地研究这些要点。

提供源代码

通常,您将在 git 或其他版本控制系统中对代码进行版本控制,并且您的版本控制代码可能在公共存储库中可用,例如 GitHub。 在这种情况下,您应该注意在生成要部署到 Internet Computer 的代码时使用的特定提交,并将其传达给尝试验证您的容器的用户。 或者,您也可以提供一个包(例如,zip 文件或 tarball),其中包含您用于构建容器的源代码。

重现构建环境

在构建代码之前,您应该详细记录您正在使用的构建环境。 特别是对于 Internet Computer SDK 支持的语言:

  • 记下您用于构建容器的操作系统及其版本。

  • 如果您使用的是 dfx,请注意使用的版本,如 dfx.json 中指定的那样。 您可以安装任意版本 使用 dfx toolchain install <version> 或使用 DFX_VERSION 运行安装脚本 环境变量设置为所需的版本。

  • 如果您以 dfx build 以外的方式构建 Motoko 代码,请注意您正在使用的 moc 版本。

  • 如果您正在构建 Rust,请注意您正在使用的 cargo 版本。

  • 如果您使用 Node.js 和/或 webpack 进行前端开发,请注意它们的版本。

  • 如果您的构建过程依赖于任何环境变量(例如时区或语言环境),请记下它们。

您应该在说明中将所有这些信息传达给您的用户。 理想情况下,通过使用 Docker 或 Nix 等工具提供一个可执行的配方来重新创建构建环境。 我们建议使用 Docker,因为这还可以让您确定用于构建软件的操作系统。

使用 Docker 构建环境

Docker 容器 是提供构建环境的流行解决方案。 您可以使用如下所示的 Dockerfile 为用户提供特定版本的操作系统, 以及 dfx、Node.js 和 Rust 工具链。

FROM ubuntu:20.10

# Install a basic environment needed for our build tools
RUN \
    apt -yq update && \
    apt -yqq install --no-install-recommends curl ca-certificates \
        build-essential pkg-config libssl-dev llvm-dev liblmdb-dev clang cmake

# Install Node.js using nvm
# Specify the Node version
ENV NODE_VERSION=14.17.1
RUN curl --fail -sSf https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
ENV NVM_DIR=/root/.nvm
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"

# Install Rust and Cargo in /opt
# Specify the Rust toolchain version
ARG rust_version=1.54.0
ENV RUSTUP_HOME=/opt/rustup \
    CARGO_HOME=/opt/cargo \
    PATH=/opt/cargo/bin:$PATH
RUN curl --fail https://sh.rustup.rs -sSf \
        | sh -s -- -y --default-toolchain ${rust_version}-x86_64-unknown-linux-gnu --no-modify-path && \
    rustup default ${rust_version}-x86_64-unknown-linux-gnu && \
    rustup target add wasm32-unknown-unknown

# Install dfx; the version is picked up from the DFX_VERSION environment variable
ENV DFX_VERSION=0.9.2
RUN sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"

COPY . /canister
WORKDIR /canister

关于这个 Dockerfile 有几点值得注意:

  • 它从一个官方的 Docker 镜像开始。 此外,所有安装的工具都是标准的, 来自标准来源。 这使用户确信构建环境没有被篡改,因此 使用 Docker 的构建过程是可以信任的。

  • 为确保安装了特定版本的构建工具,它直接安装它们,而不是 通过 apt(Ubuntu 的包管理器,在容器内运行的 Linux 发行版)。 这样的包管理器通常不提供将构建工具固定到特定版本的方法。

要使用这个 Dockerfile,请获取 Docker up and running,将 Dockerfile 放在项目目录中 您的容器,并通过运行以下命令创建 Docker 容器:

$ docker build -t mycanister .

这将创建一个名为“mycanister”的 Docker 容器映像,其中安装了 Node.js、Rust 和“dfx”,以及您的容器源代码 复制到`/canister`(记得你应该从canister项目目录调用`docker build`)。 然后,您可以通过运行以下命令进入容器内的交互式 shell:

docker run -it --rm mycanister

从这里,您可以尝试构建您的容器所需的步骤。 一旦你确信这些步骤是确定性的,你也可以把它们放在 Dockerfile 中, 允许用户在创建容器时自动复制您的构建。 您可以在 互联网身份容器的 Dockerfile 中查看示例。 接下来,我们将研究使构建具有确定性所需的条件。

确保构建过程的确定性

为了使构建过程具有确定性:

  1. 您将需要确保您的容器的任何依赖项始终以相同的方式解决。 大多数构建工具现在支持将依赖项固定到特定版本的方式。

    • 对于 npm,运行 npm install 将创建一个 package-lock.json 文件,其中包含项目的所有 传递依赖项的一些固定版本,以满足 package.json 中指定的要求。 但是,npm install 会在每次调用时覆盖 package-lock.json 文件。 因此,一旦您准备好创建容器的最终版本,只需运行一次“npm install”。 之后,将 package-lock.json 提交到您的版本控制系统。 最后,在检查构建的可重复性时,使用 npm ci 而不是 npm install

    • 对于 Rust 代码,Cargo 会自动生成一个带有固定版本的 Cargo.lock 文件 您的(传递)依赖项。 与 package-lock.json 一样,您应该将此文件提交到您的版本控制系统。 准备好生产您的容器的最终版本。 此外,默认情况下,Cargo 会忽略依赖项的锁定版本。 将 --locked 标志传递给 cargo 命令以确保使用锁定的依赖项。

    • 您必须提前分配容器 ID,因为容器通过其 ID 相互引用。

  2. 您自己的构建脚本不得引入非确定性。 不确定性的明显来源包括随机性、时间戳、并发性或代码混淆器。 不太明显的来源包括语言环境、绝对文件路径、目录中文件的顺序以及内容可以更改的远程 URL。 此外,依赖第三方构建插件会使您暴露于这些引入的任何不确定性。

  3. 给定相同的依赖关系和确定性构建脚本,构建工具本身(Motoko 的 moc ,Rust 的 cargo ,webpack` 默认情况下用于前端开发)也必须是确定性的。 好消息是所有这些工具都旨在确定性。 但是,它们是复杂的软件,确保确定性并非易事。 因此,非确定性错误可以而且确实会发生。 对于 Rust,请参阅 https://github.com/rust-lang/rust/labels/A-reproducibility [Rust 中当前潜在的非确定性问题列表]。 此外,我们观察到在 Linux 和 MacOS 下编译为 Wasm 的 Rust 代码之间存在差异,因此建议固定构建平台及其版本。 对于 webpack,确定性对于确保缓存很重要,从第 5 版开始,webpack 引入了您应该使用的 deterministic Naming of module and chunk IDs。 Motoko 编译器的目标是确定性和可重复性;如果您发现重现性问题,请提交 新问题,我们将尽可能解决这些问题。

测试重现性

如果可重现性对您的代码至关重要,您应该测试您的构建以增加您对其重现性的信心。 这样的测试并非微不足道:我们已经看到了真实世界的例子,其中容器构建中的非确定性需要一个月才能出现! 幸运的是,Debian Reproducible Builds 项目创建了一个名为 reprotest 的工具,它可以帮助您自动化再现性测试。 它通过在路径、时间、文件顺序等特性不同的两个不同环境中运行构建来测试您的构建, 并比较结果。 要使用它,您可以将以下 Dockerfile 放在容器项目的根目录中。

FROM ubuntu:20.10

# Install a basic environment needed for our build tools
RUN \
    apt -yq update && \
    apt -yqq install --no-install-recommends curl ca-certificates \
        build-essential pkg-config libssl-dev llvm-dev liblmdb-dev clang cmake

# Install Node.js using nvm
ENV NODE_VERSION=14.17.1
RUN curl --fail -sSf https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
ENV NVM_DIR=/root/.nvm
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"P

# Install Rust and Cargo in /opt
ARG rust_version=1.51.0
ENV RUSTUP_HOME=/opt/rustup \
    CARGO_HOME=/opt/cargo \
    PATH=/opt/cargo/bin:$PATH
RUN curl --fail https://sh.rustup.rs -sSf \
        | sh -s -- -y --default-toolchain ${rust_version}-x86_64-unknown-linux-gnu --no-modify-path && \
    rustup default ${rust_version}-x86_64-unknown-linux-gnu && \
    rustup target add wasm32-unknown-unknown

# Install dfx; the version is picked up the DFX_VERSION environment variable
ENV DFX_VERSION=0.9.2
RUN sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"

RUN apt -yqq install --no-install-recommends reprotest disorderfs faketime sudo wabt

COPY . /canister
WORKDIR /canister

接下来,创建一个包含 Internet Computer 上的容器 ID 的“canister_ids.json”文件,并将其放在项目目录中。 现在,从您的容器项目的根目录中,您可以测试您的“dfx”构建的可重复性,如下所示:

$ docker build -t mycanister .
...
$ docker run --rm --privileged -it mycanister
root@6fe19d89f8f5:/canister# reprotest -vv "dfx build --network ic" '.dfx/ic/canisters/*/*.wasm'

第一个命令使用上面的 Dockerfile 构建 Docker 容器。 第二个在容器中打开一个交互式 shell(因此是 -it 标志)。 我们在特权模式下运行它(--privileged 标志),因为 reprotest 使用内核模块进行一些构建环境的变化。 您还可以通过排除一些变体以非特权模式运行它;请参阅 reprotest 手册--rm 标志将在您关闭其外壳后销毁该容器。 最后,一旦进入容器,我们以详细模式启动 reprotest-vv 标志)。 您需要将要运行的构建命令作为第一个参数提供给它。 在这里,我们假设它是 dfx build --network ic - 如果您使用不同的构建过程,请调整它。 然后它将在两个不同的环境中运行构建。 最后,您需要告诉 reprotest 在两个构建结束时要比较哪些路径。 在这里,我们比较了所有容器的 Wasm 代码,该代码位于 .dfx/ic 目录中。

如果比较没有发现任何差异,您将看到与此类似的输出:

No differences in ./.dfx/ic/canisters/*/*.wasm
27ff185372dbf51a860d6ddbe6fc9cbdd47cb41fba8c1b702bed9767cc34d66f  ./.dfx/ic/canisters/Map/Map.wasm
6af1076f70407854cd6f62f23429d81f58398729f9ee5d4247ae4f93eb12770c  ./.dfx/ic/canisters/Test/Test.wasm

恭喜 - 这是一个很好的指标,表明您的构建不受环境影响! 请注意,reprotest 无法检查您的依赖项是否正确固定 - 请使用上一节中的指南。 此外,我们建议您在多个主机操作系统下运行容器 reprotest 构建并比较结果。 如果比较确实发现两个构建中生成的 Wasm 代码之间存在差异,它将输出一个差异。 然后,您可能希望使用 reprotest--store-dir 标志将输出和差异存储在可以分析它们的地方。 如果您正在努力实现可重复性,请考虑使用 DetTrace, 这是一个容器抽象,它试图使任意构建具有确定性。

最后,即使在您实现了构建的可重复性之后,还有其他事情需要考虑 从长远来看。

长期考虑

如果您希望您的容器代码能够保留多年并保持可重复性,那么可重复性可能会更高。 最大的挑战是确保您:

  1. 构建工具链在未来仍然可用。

  2. 依赖项是可用的。

  3. Toolchain 仍然运行并且仍然正确地构建您的依赖项。

发行版和包存档可能会丢弃旧版本的包,包括您的工具链及其依赖项。 网站可能会脱机,并且 URL 可能会停止工作。 因此,备份所有工具链和依赖项是谨慎的做法。 您应该考虑参与诸如 Software Heritage 之类的大规模项目。 在稍后的某个时间点,您可能需要调整构建过程(例如,通过更改 URL)以确保您的容器仍然可以构建。 即使构建更改,如果它仍然产生相同的结果,您的用户可以确信您的容器正在运行正确的代码。 如果您的依赖项来自可信赖的来源,例如 Software Heritage 项目,则信任参数会更容易。

总结

总结我们对容器作者的建议:

  • 理想情况下,在生成容器代码的最终版本时,使用 Docker 或类似技术方便地设置 操作系统和构建工具,并为用户修复它们的版本。 如果您使用的构建工具不能保证完全可重现的构建,Docker 还可以通过最小化路径、环境变量等方面的差异来提供帮助。

  • 构建工具和基础 Docker 镜像应该来自用户可以信任的地方。

  • Rust 和 Motoko 编译器的目标是确定性,因此支持可重现的构建。如果您发现不确定性,请提交错误报告。

  • 使用 NPM 时,请确保指定所有依赖项的确切版本(将 package_lock.json 提交到 git 存储库!)。 使用 ci 命令而不是 install 调用 NPM 来重现构建。 同样,对于 Rust 包,将 Cargo.lock 提交到您的存储库,然后在构建包时使用 cargo build --locked

  • Webpack 构建应该是确定性的,但混淆器和类似工具可能会影响可重复性。 确保使用确定性块和模块 ID。

  • 构建工具并不完美,可能无法确保可重现的构建。 如果可重复性对您的容器至关重要(例如,它持有其他用户的资金),请对其进行测试。 再抗议是用于此目的的有用工具。

  • 理想情况下,您希望最小化依赖项的数量,因为为了进行全面审计,用户可能必须(可重复地)重建所有 你的依赖关系。

  • 在更长的时间范围内实现再现性更难,主要是因为您需要确保您的可靠来源 依赖项和构建工具保持可用。

最后,如果您的构建是可重现的,您可以将生成的 Wasm 代码的哈希值与容器中运行的代码的哈希值进行比较,您可以按如下方式检索它:

$ dfx canister --network ic info <canister-id>

请注意,如果控制器升级容器代码,此哈希可能会更改。