Add a crate to test the runtime can be reproducibly built

This commit is contained in:
Luke Parker 2023-07-27 14:19:03 -04:00
parent 09a95c9bd2
commit a8c7bb96c8
No known key found for this signature in database
13 changed files with 242 additions and 8 deletions

View file

@ -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/**"

View file

@ -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/**"

View file

@ -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

11
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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`.

View file

@ -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

View file

@ -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

View file

@ -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"]

View file

@ -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"),
};

View file

@ -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 <lukeparker5132@gmail.com>"]
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"] }

View file

@ -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 <http://www.gnu.org/licenses/>.

View file

@ -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::<Vec<_>>();
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);
});
}