diff --git a/.github/workflows/message-queue-tests.yml b/.github/workflows/message-queue-tests.yml index 22758cbe..da7e65db 100644 --- a/.github/workflows/message-queue-tests.yml +++ b/.github/workflows/message-queue-tests.yml @@ -8,6 +8,7 @@ on: - "common/**" - "crypto/**" - "message-queue/**" + - "orchestration/message-queue/**" - "tests/docker/**" - "tests/message-queue/**" @@ -16,6 +17,7 @@ on: - "common/**" - "crypto/**" - "message-queue/**" + - "orchestration/message-queue/**" - "tests/docker/**" - "tests/message-queue/**" diff --git a/.github/workflows/processor-tests.yml b/.github/workflows/processor-tests.yml index 1494bcf8..672315b6 100644 --- a/.github/workflows/processor-tests.yml +++ b/.github/workflows/processor-tests.yml @@ -9,7 +9,9 @@ on: - "crypto/**" - "coins/**" - "message-queue/**" + - "orchestration/message-queue/**" - "processor/**" + - "orchestration/processor/**" - "tests/docker/**" - "tests/processor/**" @@ -19,7 +21,9 @@ on: - "crypto/**" - "coins/**" - "message-queue/**" + - "orchestration/message-queue/**" - "processor/**" + - "orchestration/processor/**" - "tests/docker/**" - "tests/processor/**" diff --git a/.github/workflows/reproducible-runtime.yml b/.github/workflows/reproducible-runtime.yml new file mode 100644 index 00000000..8c6dccdd --- /dev/null +++ b/.github/workflows/reproducible-runtime.yml @@ -0,0 +1,38 @@ +name: Reproducible Runtime + +on: + push: + branches: + - develop + paths: + - "Cargo.lock" + - "common/**" + - "crypto/**" + - "substrate/**" + - "orchestration/runtime/**" + - "tests/reproducible-runtime/**" + + pull_request: + paths: + - "Cargo.lock" + - "common/**" + - "crypto/**" + - "substrate/**" + - "orchestration/runtime/**" + - "tests/reproducible-runtime/**" + + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Build Dependencies + uses: ./.github/actions/build-dependencies + with: + github-token: ${{ inputs.github-token }} + + - name: Run Reproducible Runtime tests + run: cd tests/reproducible-runtime && GITHUB_CI=true cargo test diff --git a/Cargo.lock b/Cargo.lock index 316842d8..e9a85011 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8865,6 +8865,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "serai-reproducible-runtime" +version = "0.1.0" +dependencies = [ + "dockertest", + "hex", + "rand_core 0.6.4", + "serai-docker-tests", + "tokio", +] + [[package]] name = "serai-runtime" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index ae1ad8f3..09d6d432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ members = [ "tests/docker", "tests/message-queue", "tests/processor", + "tests/reproducible-runtime", ] # Always compile Monero (and a variety of dependencies) with optimizations due diff --git a/orchestration/README.md b/orchestration/README.md index a09f2463..5420f84c 100644 --- a/orchestration/README.md +++ b/orchestration/README.md @@ -52,15 +52,10 @@ and signatures. * Expose necessary ports. * Map necessary volumes. -The best way is to build using `docker compose`. If you'd prefer to build using -`docker` directly, each image can be built independently. - -**Example:** `docker build ./coins/bitcoin` - ### Entrypoint The Serai node and external networks' nodes are each started from an entrypoint -script inside the /scripts folder. +script inside the `/scripts `folder. To update the scripts on the image you must rebuild the updated images using the `--build` flag after `up` in `docker compose`. diff --git a/orchestration/docker-compose.yml b/orchestration/docker-compose.yml index 4c3d16c6..6c88f46d 100644 --- a/orchestration/docker-compose.yml +++ b/orchestration/docker-compose.yml @@ -76,7 +76,19 @@ services: - "./processor/scripts:/scripts" entrypoint: /scripts/entry-dev.sh - # Serai services + # Serai runtime + + runtime: + profiles: + - runtime + build: + context: ../ + dockerfile: ./orchestration/runtime/Dockerfile + entrypoint: | + sh -c "cd /serai/substrate/runtime && cargo clean && cargo build --release && \ + sha256sum /serai/target/release/wbuild/serai-runtime/serai_runtime.wasm" + + # Serai nodes _serai: &serai_defaults diff --git a/orchestration/runtime/Dockerfile b/orchestration/runtime/Dockerfile new file mode 100644 index 00000000..7e6de055 --- /dev/null +++ b/orchestration/runtime/Dockerfile @@ -0,0 +1,28 @@ +FROM rust:1.71.0-slim-bookworm as builder + +# Add files for build +ADD common /serai/common +ADD crypto /serai/crypto +ADD coins /serai/coins +ADD message-queue /serai/message-queue +ADD processor /serai/processor +ADD coordinator /serai/coordinator +ADD substrate /serai/substrate +ADD tests /serai/tests +ADD Cargo.toml /serai +ADD Cargo.lock /serai +ADD AGPL-3.0 /serai + +WORKDIR /serai + +# Move to a Debian package snapshot +RUN rm -rf /etc/apt/sources.list.d/debian.sources && \ + rm -rf /var/lib/apt/lists/* && \ + echo "deb [arch=amd64] http://snapshot.debian.org/archive/debian/20230703T000000Z bookworm main" > /etc/apt/sources.list && \ + apt update + +# Install dependencies +RUN apt install clang -y + +# Add the wasm toolchain +RUN rustup target add wasm32-unknown-unknown diff --git a/substrate/primitives/Cargo.toml b/substrate/primitives/Cargo.toml index ebdc6c72..2cd16ade 100644 --- a/substrate/primitives/Cargo.toml +++ b/substrate/primitives/Cargo.toml @@ -26,5 +26,5 @@ sp-core = { git = "https://github.com/serai-dex/substrate", default-features = f sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } [features] -std = ["lazy_static", "zeroize", "scale/std", "scale-info/std", "serde/std", "sp-core/std", "sp-runtime/std"] +std = ["lazy_static", "zeroize", "scale/std", "serde/std", "scale-info/std", "sp-core/std", "sp-runtime/std"] default = ["std"] diff --git a/tests/docker/src/lib.rs b/tests/docker/src/lib.rs index 9429226c..f6885124 100644 --- a/tests/docker/src/lib.rs +++ b/tests/docker/src/lib.rs @@ -79,6 +79,11 @@ pub fn build(name: String) { meta(repo_path.join("message-queue")), meta(repo_path.join("processor")), ], + "runtime" => vec![ + meta(repo_path.join("common")), + meta(repo_path.join("crypto")), + meta(repo_path.join("substrate")), + ], _ => panic!("building unrecognized docker image"), }; diff --git a/tests/reproducible-runtime/Cargo.toml b/tests/reproducible-runtime/Cargo.toml new file mode 100644 index 00000000..44808a49 --- /dev/null +++ b/tests/reproducible-runtime/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "serai-reproducible-runtime" +version = "0.1.0" +description = "Tests the Serai runtimee can be reproducibly built" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/tests/reproducible-runtime" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +rand_core = "0.6" +hex = "0.4" + +dockertest = "0.3" +serai-docker-tests = { path = "../docker" } + +tokio = { version = "1", features = ["time"] } diff --git a/tests/reproducible-runtime/LICENSE b/tests/reproducible-runtime/LICENSE new file mode 100644 index 00000000..f684d027 --- /dev/null +++ b/tests/reproducible-runtime/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2023 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/tests/reproducible-runtime/src/lib.rs b/tests/reproducible-runtime/src/lib.rs new file mode 100644 index 00000000..72d8f9a7 --- /dev/null +++ b/tests/reproducible-runtime/src/lib.rs @@ -0,0 +1,100 @@ +#[test] +pub fn reproducibly_builds() { + use std::{collections::HashSet, process::Command}; + + use rand_core::{RngCore, OsRng}; + + use dockertest::{PullPolicy, Image, Composition, DockerTest}; + + const RUNS: usize = 3; + const TIMEOUT: u16 = 60 * 60; // 60 minutes + + serai_docker_tests::build("runtime".to_string()); + + let mut ids = vec![[0; 8]; RUNS]; + for id in &mut ids { + OsRng.fill_bytes(id); + } + + let mut test = DockerTest::new(); + for id in &ids { + test.add_composition( + Composition::with_image( + Image::with_repository("serai-dev-runtime").pull_policy(PullPolicy::Never), + ) + .with_container_name(format!("runtime-build-{}", hex::encode(id))) + .with_cmd(vec![ + "sh".to_string(), + "-c".to_string(), + // Sleep for a minute after building to prevent the container from closing before we + // retrieve the hash + "cd /serai/substrate/runtime && cargo clean && cargo build --release && + printf \"Runtime hash: \" > hash && + sha256sum /serai/target/release/wbuild/serai-runtime/serai_runtime.wasm >> hash && + cat hash && + sleep 60" + .to_string(), + ]), + ); + } + + test.run(|_| async { + let ids = ids; + let mut containers = vec![]; + for container in String::from_utf8( + Command::new("docker").arg("ps").arg("--format").arg("{{.Names}}").output().unwrap().stdout, + ) + .expect("output wasn't utf-8") + .lines() + { + for id in &ids { + if container.contains(&hex::encode(id)) { + containers.push(container.trim().to_string()); + } + } + } + assert_eq!(containers.len(), RUNS, "couldn't find all containers"); + + let mut res = vec![None; RUNS]; + 'attempt: for _ in 0 .. (TIMEOUT / 10) { + tokio::time::sleep(core::time::Duration::from_secs(10)).await; + + 'runner: for (i, container) in containers.iter().enumerate() { + if res[i].is_some() { + continue; + } + + let logs = Command::new("docker").arg("logs").arg(container).output().unwrap(); + let Some(last_log) = + std::str::from_utf8(&logs.stdout).expect("output wasn't utf-8").lines().last() else { + continue 'runner; + }; + + let split = last_log.split("Runtime hash: ").collect::>(); + if split.len() == 2 { + res[i] = Some(split[1].to_string()); + continue 'runner; + } + } + + for item in &res { + if item.is_none() { + continue 'attempt; + } + } + break; + } + + // If we didn't get resuts from all runners, panic + for item in &res { + if item.is_none() { + panic!("couldn't get runtime hashes within allowed time"); + } + } + let mut identical = HashSet::new(); + for res in res.clone() { + identical.insert(res.unwrap()); + } + assert_eq!(identical.len(), 1, "got different runtime hashes {:?}", res); + }); +}