diff --git a/.github/workflows/message-queue-tests.yml b/.github/workflows/message-queue-tests.yml index f9042a93..87d50672 100644 --- a/.github/workflows/message-queue-tests.yml +++ b/.github/workflows/message-queue-tests.yml @@ -6,11 +6,13 @@ on: - develop paths: - "message-queue/**" + - "tests/docker/**" - "tests/message-queue/**" pull_request: paths: - "message-queue/**" + - "tests/docker/**" - "tests/message-queue/**" workflow_dispatch: diff --git a/.github/workflows/processor-tests.yml b/.github/workflows/processor-tests.yml new file mode 100644 index 00000000..9640b841 --- /dev/null +++ b/.github/workflows/processor-tests.yml @@ -0,0 +1,32 @@ +name: Processor Tests + +on: + push: + branches: + - develop + paths: + - "processor/**" + - "tests/docker/**" + - "tests/processor/**" + + pull_request: + paths: + - "processor/**" + - "tests/docker/**" + - "tests/processor/**" + + 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 processor Docker tests + run: cd tests/processor && cargo test diff --git a/Cargo.lock b/Cargo.lock index f8259006..0795297f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8736,6 +8736,10 @@ dependencies = [ "rocksdb", ] +[[package]] +name = "serai-docker-tests" +version = "0.1.0" + [[package]] name = "serai-env" version = "0.1.0" @@ -8805,6 +8809,7 @@ dependencies = [ "dockertest", "hex", "rand_core 0.6.4", + "serai-docker-tests", "serai-message-queue", "serai-primitives", "tokio", @@ -8931,6 +8936,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "serai-processor-tests" +version = "0.1.0" +dependencies = [ + "ciphersuite", + "dkg", + "dockertest", + "hex", + "rand_core 0.6.4", + "serai-docker-tests", + "serai-message-queue", + "serai-message-queue-tests", + "serai-primitives", + "serai-processor-messages", + "serai-validator-sets-primitives", + "serde_json", + "tokio", +] + [[package]] name = "serai-runtime" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c5a566da..ae1ad8f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,10 @@ members = [ "substrate/client", "tests/no-std", + + "tests/docker", "tests/message-queue", + "tests/processor", ] # Always compile Monero (and a variety of dependencies) with optimizations due diff --git a/deny.toml b/deny.toml index c27ee4f5..79ce5989 100644 --- a/deny.toml +++ b/deny.toml @@ -66,7 +66,9 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-client" }, + { allow = ["AGPL-3.0"], name = "serai-docker-tests" }, { allow = ["AGPL-3.0"], name = "serai-message-queue-tests" }, + { allow = ["AGPL-3.0"], name = "serai-processor-tests" }, ] [[licenses.clarify]] diff --git a/tests/docker/Cargo.toml b/tests/docker/Cargo.toml new file mode 100644 index 00000000..5058c42e --- /dev/null +++ b/tests/docker/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "serai-docker-tests" +version = "0.1.0" +description = "Docker-based testing infrastructure for Serai" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/tests/docker" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/tests/docker/LICENSE b/tests/docker/LICENSE new file mode 100644 index 00000000..f684d027 --- /dev/null +++ b/tests/docker/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/docker/src/lib.rs b/tests/docker/src/lib.rs new file mode 100644 index 00000000..a2436d62 --- /dev/null +++ b/tests/docker/src/lib.rs @@ -0,0 +1,45 @@ +use std::{ + sync::{Mutex, OnceLock}, + collections::HashMap, + env, +}; + +static BUILT: OnceLock>> = OnceLock::new(); +pub fn build(name: String) { + let built = BUILT.get_or_init(|| Mutex::new(HashMap::new())); + // Only one call to build will acquire this lock + let mut built_lock = built.lock().unwrap(); + if built_lock.contains_key(&name) { + // If it was built, return + return; + } + + // Else, hold the lock while we build + let mut path = env::current_exe().unwrap(); + path.pop(); + assert!(path.as_path().ends_with("deps")); + path.pop(); + assert!(path.as_path().ends_with("debug")); + path.pop(); + assert!(path.as_path().ends_with("target")); + path.pop(); + path.push("deploy"); + + println!("Building {}...", &name); + + assert!(std::process::Command::new("docker") + .current_dir(path) + .arg("compose") + .arg("build") + .arg(&name) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + + println!("Built!"); + + // Set built + built_lock.insert(name, true); +} diff --git a/tests/message-queue/Cargo.toml b/tests/message-queue/Cargo.toml index 2163abe4..71bb7ffa 100644 --- a/tests/message-queue/Cargo.toml +++ b/tests/message-queue/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "serai-message-queue-tests" version = "0.1.0" -description = "Tests for Serai's message-queue" +description = "Tests for Serai's Message Queue" license = "AGPL-3.0-only" repository = "https://github.com/serai-dex/serai/tree/develop/tests/message-queue" authors = ["Luke Parker "] @@ -25,3 +25,4 @@ serai-message-queue = { path = "../../message-queue" } tokio = { version = "1", features = ["full"] } dockertest = "0.3" +serai-docker-tests = { path = "../docker" } diff --git a/tests/message-queue/src/lib.rs b/tests/message-queue/src/lib.rs index ee20d002..2b1a03a4 100644 --- a/tests/message-queue/src/lib.rs +++ b/tests/message-queue/src/lib.rs @@ -1,176 +1,125 @@ -#[cfg(test)] -mod tests { - use std::{ - sync::{Mutex, OnceLock}, - collections::HashMap, - env, - }; +use std::collections::HashMap; - use rand_core::OsRng; +use rand_core::OsRng; - use ciphersuite::{ - group::{ - ff::{Field, PrimeField}, - GroupEncoding, - }, - Ciphersuite, Ristretto, - }; +use ciphersuite::{ + group::{ff::Field, GroupEncoding}, + Ciphersuite, Ristretto, +}; + +use serai_primitives::NetworkId; + +use dockertest::{PullPolicy, Image, LogAction, LogPolicy, LogSource, LogOptions, Composition}; + +pub type MessageQueuePrivateKey = ::F; +pub fn instance( +) -> (MessageQueuePrivateKey, HashMap, Composition) { + serai_docker_tests::build("message-queue".to_string()); + + let coord_key = ::F::random(&mut OsRng); + let priv_keys = HashMap::from([ + (NetworkId::Bitcoin, ::F::random(&mut OsRng)), + (NetworkId::Ethereum, ::F::random(&mut OsRng)), + (NetworkId::Monero, ::F::random(&mut OsRng)), + ]); + + let mut composition = Composition::with_image( + Image::with_repository("serai-dev-message-queue").pull_policy(PullPolicy::Never), + ) + .with_log_options(Some(LogOptions { + action: LogAction::Forward, + policy: LogPolicy::Always, + source: LogSource::Both, + })) + .with_env( + [ + ("COORDINATOR_KEY".to_string(), hex::encode((Ristretto::generator() * coord_key).to_bytes())), + ( + "BITCOIN_KEY".to_string(), + hex::encode((Ristretto::generator() * priv_keys[&NetworkId::Bitcoin]).to_bytes()), + ), + ( + "ETHEREUM_KEY".to_string(), + hex::encode((Ristretto::generator() * priv_keys[&NetworkId::Ethereum]).to_bytes()), + ), + ( + "MONERO_KEY".to_string(), + hex::encode((Ristretto::generator() * priv_keys[&NetworkId::Monero]).to_bytes()), + ), + ("DB_PATH".to_string(), "./message-queue-db".to_string()), + ] + .into(), + ); + composition.publish_all_ports(); + + (coord_key, priv_keys, composition) +} + +#[test] +fn basic_functionality() { + use std::env; + + use ciphersuite::group::ff::PrimeField; + + use dockertest::DockerTest; - use serai_primitives::NetworkId; use serai_message_queue::{Service, Metadata, client::MessageQueue}; - use dockertest::{ - PullPolicy, Image, LogAction, LogPolicy, LogSource, LogOptions, Composition, DockerTest, - }; + let mut test = DockerTest::new(); + let (coord_key, priv_keys, composition) = instance(); + test.add_composition(composition); + test.run(|ops| async move { + // Sleep for a second for the message-queue to boot + // It isn't an error to start immediately, it just silences an error + tokio::time::sleep(core::time::Duration::from_secs(1)).await; - static BUILT: OnceLock> = OnceLock::new(); - fn build() { - let built = BUILT.get_or_init(|| Mutex::new(false)); - // Only one call to build will acquire this lock - let mut built_lock = built.lock().unwrap(); - if *built_lock { - // If it was built, return - return; - } + let rpc = ops.handle("serai-dev-message-queue").host_port(2287).unwrap(); + // TODO: Add new to MessageQueue to avoid needing to use set_var + env::set_var("MESSAGE_QUEUE_RPC", rpc.0.to_string() + ":" + &rpc.1.to_string()); + env::set_var("MESSAGE_QUEUE_KEY", hex::encode(coord_key.to_repr())); - // Else, hold the lock while we build - let mut path = env::current_exe().unwrap(); - path.pop(); - assert!(path.as_path().ends_with("deps")); - path.pop(); - assert!(path.as_path().ends_with("debug")); - path.pop(); - assert!(path.as_path().ends_with("target")); - path.pop(); - path.push("deploy"); + // Queue some messages + let coordinator = MessageQueue::from_env(Service::Coordinator); + coordinator + .queue( + Metadata { + from: Service::Coordinator, + to: Service::Processor(NetworkId::Bitcoin), + intent: b"intent".to_vec(), + }, + b"Hello, World!".to_vec(), + ) + .await; - println!("Building message-queue..."); + coordinator + .queue( + Metadata { + from: Service::Coordinator, + to: Service::Processor(NetworkId::Bitcoin), + intent: b"intent 2".to_vec(), + }, + b"Hello, World, again!".to_vec(), + ) + .await; - assert!(std::process::Command::new("docker") - .current_dir(path) - .arg("compose") - .arg("build") - .arg("message-queue") - .spawn() - .unwrap() - .wait() - .unwrap() - .success()); + // Successfully get it + env::set_var("MESSAGE_QUEUE_KEY", hex::encode(priv_keys[&NetworkId::Bitcoin].to_repr())); + let bitcoin = MessageQueue::from_env(Service::Processor(NetworkId::Bitcoin)); + let msg = bitcoin.next(0).await; + assert_eq!(msg.from, Service::Coordinator); + assert_eq!(msg.id, 0); + assert_eq!(&msg.msg, b"Hello, World!"); - println!("Built!"); + // If we don't ack it, it should continue to be returned + assert_eq!(msg, bitcoin.next(0).await); - // Set built - *built_lock = true; - } + // Acknowledging it should yield the next message + bitcoin.ack(0).await; - type PrivateKey = ::F; - fn instance() -> (PrivateKey, HashMap, Composition) { - build(); - - let coord_key = ::F::random(&mut OsRng); - let priv_keys = HashMap::from([ - (NetworkId::Bitcoin, ::F::random(&mut OsRng)), - (NetworkId::Ethereum, ::F::random(&mut OsRng)), - (NetworkId::Monero, ::F::random(&mut OsRng)), - ]); - - let mut composition = Composition::with_image( - Image::with_repository("serai-dev-message-queue").pull_policy(PullPolicy::Never), - ) - .with_log_options(Some(LogOptions { - action: LogAction::Forward, - policy: LogPolicy::Always, - source: LogSource::Both, - })) - .with_env( - [ - ( - "COORDINATOR_KEY".to_string(), - hex::encode((Ristretto::generator() * coord_key).to_bytes()), - ), - ( - "BITCOIN_KEY".to_string(), - hex::encode((Ristretto::generator() * priv_keys[&NetworkId::Bitcoin]).to_bytes()), - ), - ( - "ETHEREUM_KEY".to_string(), - hex::encode((Ristretto::generator() * priv_keys[&NetworkId::Ethereum]).to_bytes()), - ), - ( - "MONERO_KEY".to_string(), - hex::encode((Ristretto::generator() * priv_keys[&NetworkId::Monero]).to_bytes()), - ), - ("DB_PATH".to_string(), "./message-queue-db".to_string()), - ] - .into(), - ); - composition.publish_all_ports(); - - (coord_key, priv_keys, composition) - } - - #[test] - fn basic_functionality() { - let mut test = DockerTest::new(); - let (coord_key, priv_keys, composition) = instance(); - test.add_composition(composition); - test.run(|ops| async move { - // Sleep for a second for the message-queue to boot - // It isn't an error to start immediately, it just silences an error - tokio::time::sleep(core::time::Duration::from_secs(1)).await; - - let rpc = ops.handle("serai-dev-message-queue").host_port(2287).unwrap(); - // TODO: MessageQueue directly read from env to remove this boilerplate from all binaries, - // yet it's now annoying as hell to parameterize. Split into new/from_env? - env::set_var( - "MESSAGE_QUEUE_RPC", - "http://".to_string() + &rpc.0.to_string() + ":" + &rpc.1.to_string(), - ); - env::set_var("MESSAGE_QUEUE_KEY", hex::encode(coord_key.to_repr())); - - // Queue some messagse - let coordinator = MessageQueue::new(Service::Coordinator); - coordinator - .queue( - Metadata { - from: Service::Coordinator, - to: Service::Processor(NetworkId::Bitcoin), - intent: b"intent".to_vec(), - }, - b"Hello, World!".to_vec(), - ) - .await; - - coordinator - .queue( - Metadata { - from: Service::Coordinator, - to: Service::Processor(NetworkId::Bitcoin), - intent: b"intent 2".to_vec(), - }, - b"Hello, World, again!".to_vec(), - ) - .await; - - // Successfully get it - env::set_var("MESSAGE_QUEUE_KEY", hex::encode(priv_keys[&NetworkId::Bitcoin].to_repr())); - let bitcoin = MessageQueue::new(Service::Processor(NetworkId::Bitcoin)); - let msg = bitcoin.next(0).await; - assert_eq!(msg.from, Service::Coordinator); - assert_eq!(msg.id, 0); - assert_eq!(&msg.msg, b"Hello, World!"); - - // If we don't ack it, it should continue to be returned - assert_eq!(msg, bitcoin.next(0).await); - - // Acknowledging it should yield the next message - bitcoin.ack(0).await; - - let next_msg = bitcoin.next(1).await; - assert!(msg != next_msg); - assert_eq!(next_msg.from, Service::Coordinator); - assert_eq!(next_msg.id, 1); - assert_eq!(&next_msg.msg, b"Hello, World, again!"); - }); - } + let next_msg = bitcoin.next(1).await; + assert!(msg != next_msg); + assert_eq!(next_msg.from, Service::Coordinator); + assert_eq!(next_msg.id, 1); + assert_eq!(&next_msg.msg, b"Hello, World, again!"); + }); } diff --git a/tests/processor/Cargo.toml b/tests/processor/Cargo.toml new file mode 100644 index 00000000..a42457c8 --- /dev/null +++ b/tests/processor/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "serai-processor-tests" +version = "0.1.0" +description = "Tests for Serai's Processor" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/tests/processor" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +hex = "0.4" + +rand_core = "0.6" + +ciphersuite = { path = "../../crypto/ciphersuite", features = ["ristretto"] } +dkg = { path = "../../crypto/dkg" } + +messages = { package = "serai-processor-messages", path = "../../processor/messages" } + +serai-primitives = { path = "../../substrate/primitives" } +serai-validator-sets-primitives = { path = "../../substrate/validator-sets/primitives" } +serai-message-queue = { path = "../../message-queue" } + +serde_json = "1" + +tokio = { version = "1", features = ["full"] } + +dockertest = "0.3" +serai-docker-tests = { path = "../docker" } +serai-message-queue-tests = { path = "../message-queue" } diff --git a/tests/processor/LICENSE b/tests/processor/LICENSE new file mode 100644 index 00000000..f684d027 --- /dev/null +++ b/tests/processor/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/processor/src/lib.rs b/tests/processor/src/lib.rs new file mode 100644 index 00000000..71dfa7b9 --- /dev/null +++ b/tests/processor/src/lib.rs @@ -0,0 +1,142 @@ +use rand_core::{RngCore, OsRng}; + +use ciphersuite::{group::ff::PrimeField, Ciphersuite, Ristretto}; + +use dockertest::{ + PullPolicy, Image, LogAction, LogPolicy, LogSource, LogOptions, StartPolicy, Composition, +}; + +pub fn bitcoin_instance() -> Composition { + serai_docker_tests::build("bitcoin".to_string()); + + Composition::with_image( + Image::with_repository("serai-dev-bitcoin").pull_policy(PullPolicy::Never), + ) + .with_log_options(Some(LogOptions { + action: LogAction::Forward, + policy: LogPolicy::Always, + source: LogSource::Both, + })) + .with_cmd(vec![ + "bitcoind".to_string(), + "-txindex".to_string(), + "-regtest".to_string(), + "-rpcuser=serai".to_string(), + "-rpcpassword=seraidex".to_string(), + "-rpcbind=0.0.0.0".to_string(), + "-rpcallowip=0.0.0.0/0".to_string(), + "-rpcport=8332".to_string(), + ]) + .with_start_policy(StartPolicy::Strict) +} + +pub fn instance(message_queue_key: ::F) -> Composition { + serai_docker_tests::build("processor".to_string()); + + let mut entropy = [0; 32]; + OsRng.fill_bytes(&mut entropy); + + Composition::with_image( + Image::with_repository("serai-dev-processor").pull_policy(PullPolicy::Never), + ) + .with_log_options(Some(LogOptions { + action: LogAction::Forward, + policy: LogPolicy::Always, + source: LogSource::Both, + })) + .with_env( + [ + ("MESSAGE_QUEUE_KEY".to_string(), hex::encode(message_queue_key.to_repr())), + ("ENTROPY".to_string(), hex::encode(entropy)), + ("NETWORK".to_string(), "bitcoin".to_string()), + ("NETWORK_RPC_LOGIN".to_string(), "serai:seraidex".to_string()), + ("NETWORK_RPC_PORT".to_string(), "8332".to_string()), + ("DB_PATH".to_string(), "./processor-db".to_string()), + ] + .into(), + ) + .with_start_policy(StartPolicy::Strict) +} + +#[test] +fn basic_functionality() { + use std::env; + + use serai_primitives::NetworkId; + use serai_validator_sets_primitives::{Session, ValidatorSet}; + + use serai_message_queue::{Service, Metadata, client::MessageQueue}; + + use dockertest::DockerTest; + + let bitcoin_composition = bitcoin_instance(); + + let (coord_key, message_queue_keys, message_queue_composition) = + serai_message_queue_tests::instance(); + let message_queue_composition = message_queue_composition.with_start_policy(StartPolicy::Strict); + + let mut processor_composition = instance(message_queue_keys[&NetworkId::Bitcoin]); + processor_composition.inject_container_name(bitcoin_composition.handle(), "NETWORK_RPC_HOSTNAME"); + processor_composition + .inject_container_name(message_queue_composition.handle(), "MESSAGE_QUEUE_RPC"); + + let mut test = DockerTest::new(); + test.add_composition(bitcoin_composition); + test.add_composition(message_queue_composition); + test.add_composition(processor_composition); + + test.run(|ops| async move { + // Sleep for 10 seconds to be polite and let things boot + tokio::time::sleep(core::time::Duration::from_secs(10)).await; + + // Connect to the Message Queue as the coordinator + let rpc = ops.handle("serai-dev-message-queue").host_port(2287).unwrap(); + // TODO: MessageQueue::new + env::set_var( + "MESSAGE_QUEUE_RPC", + "http://".to_string() + &rpc.0.to_string() + ":" + &rpc.1.to_string(), + ); + env::set_var("MESSAGE_QUEUE_KEY", hex::encode(coord_key.to_repr())); + let coordinator = MessageQueue::from_env(Service::Coordinator); + + // Order a key gen + let id = messages::key_gen::KeyGenId { + set: ValidatorSet { session: Session(0), network: NetworkId::Bitcoin }, + attempt: 0, + }; + + coordinator + .queue( + Metadata { + from: Service::Coordinator, + to: Service::Processor(NetworkId::Bitcoin), + intent: b"key_gen_0".to_vec(), + }, + serde_json::to_string(&messages::CoordinatorMessage::KeyGen( + messages::key_gen::CoordinatorMessage::GenerateKey { + id, + params: dkg::ThresholdParams::new(3, 4, dkg::Participant::new(1).unwrap()).unwrap(), + }, + )) + .unwrap() + .into_bytes(), + ) + .await; + + // Read the created commitments + let msg = coordinator.next(0).await; + assert_eq!(msg.from, Service::Processor(NetworkId::Bitcoin)); + assert_eq!(msg.id, 0); + let msg: messages::ProcessorMessage = serde_json::from_slice(&msg.msg).unwrap(); + match msg { + messages::ProcessorMessage::KeyGen(messages::key_gen::ProcessorMessage::Commitments { + id: this_id, + commitments: _, + }) => { + assert_eq!(this_id, id); + } + _ => panic!("processor didn't return Commitments in response to GenerateKey"), + } + coordinator.ack(0).await; + }); +}